惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

美团技术团队
P
Proofpoint News Feed
WordPress大学
WordPress大学
P
Proofpoint News Feed
宝玉的分享
宝玉的分享
腾讯CDC
罗磊的独立博客
P
Privacy International News Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Latest news
Latest news
C
Cisco Blogs
Project Zero
Project Zero
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
博客园 - 叶小钗
Spread Privacy
Spread Privacy
博客园 - 三生石上(FineUI控件)
Blog — PlanetScale
Blog — PlanetScale
C
Check Point Blog
博客园 - 聂微东
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tenable Blog
Know Your Adversary
Know Your Adversary
Security Latest
Security Latest
T
The Blog of Author Tim Ferriss
云风的 BLOG
云风的 BLOG
L
Lohrmann on Cybersecurity
IT之家
IT之家
T
Tor Project blog
S
SegmentFault 最新的问题
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
月光博客
月光博客
L
LINUX DO - 热门话题
C
CERT Recently Published Vulnerability Notes
T
Threat Research - Cisco Blogs
A
Arctic Wolf
D
DataBreaches.Net
C
Cybersecurity and Infrastructure Security Agency CISA
博客园 - 【当耐特】
T
The Exploit Database - CXSecurity.com
T
Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
Scott Helme
Scott Helme
博客园 - 司徒正美
Vercel News
Vercel News
H
Help Net Security
N
News and Events Feed by Topic
MyScale Blog
MyScale Blog
Simon Willison's Weblog
Simon Willison's Weblog
P
Privacy & Cybersecurity Law Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Kawai-Focus 2.6: путь к MVP1 — создание экрана Таймер
Arduinum · 2026-04-24 · via Все публикации подряд на Хабре

Вступление

Всем доброго дня! В предыдущей статье Kawai-Focus 2.5: сборка и упаковка Tauri-приложения (Windows + Arch Linux, AUR):

  1. Собрано приложение под Windows;

  2. Отправлен рецепт в Aur для Linux Arch.

Мне удалось собрать приложение под большинство популярных операционных систем, за исключением, пожалуй, macOS. Однако на данный момент функциональность остаётся довольно скромной: приложение позволяет лишь просматривать список демонстрационных таймеров и информацию о них. До MVP1 ему ещё далеко, поэтому сейчас моя задача — расширить его возможности.

Напомню, что я разрабатываю приложение для работы с Pomodoro-таймерами, которые помогают фокусироваться на задачах в течение заданных промежутков времени. Сегодня я начну работу над экраном «Таймер», который пока будет воспроизводить только демонстрационные таймеры, доступные на экране «Таймеры».

В данной статье я сосредоточусь на внешнем виде экрана, а также на механизме обратного отсчёта часов, минут и секунд таймера и кнопках управления. Также мне предстоит написать код, который получает данные таймера из базы данных SQLite3 по его id и осуществляет переход на экран таймера.

Заваривайте чай, доставайте вкусняшки — пора «расширять грядку для помидоров»! 🍅


Работа над экраном Таймер

Я пишу приложение, которое работает с таймерами по технике Pomodoro (или «помидора»). Pomodoro — это техника управления временем, при которой работа делится на интервалы, или «помидоры» (обычно по 25 минут), разделённые короткими перерывами. После нескольких таких циклов следует более длительный перерыв.

Такой подход помогает сохранять концентрацию, снижать утомляемость и повышает эффективность выполнения задач. Однако работа фиксированными интервалами подходит не всем, поэтому технику организации времени лучше подбирать индивидуально.

Мне, как фрилансеру, этот подход помогает равномерно распределять время между проектами в течение рабочего дня. Поэтому я разрабатываю это приложение не только как разработчик, но и как пользователь — для себя.

Идея экрана Таймер

Я работаю над проектом не с нуля. У меня уже есть прототип, написанный на Python с использованием фреймворков Kivy и KivyMD. Однако я решил переписать приложение на другой стек, так как вёрстка и сборка показались мне неудобными, а при отрисовке интерфейса иногда возникали заметные тормоза.

О старом проекте я написал несколько статей, поэтому, если вам интересно более глубокое раскрытие идей, можете ознакомиться с ними, начиная со статьи Kawai-Focus 1: приложение для фокусировки внимания. В текущей статье я сосредоточусь на переписывании приложения под новый стек, а не на повторном описании уже рассмотренных идей.

Функциональность экрана «Таймер» должна отражать основную суть техники Pomodoro — последовательное воспроизведение цепочки таймеров.

Основная функциональность:

  • Обратный отсчёт времени с отображением на экране;

  • Воспроизведение звука по завершении таймера;

  • Управление таймером с помощью кнопок: «Старт», «Стоп», «Пауза».

В моём старом приложении экран Таймер до нажатия кнопки Старт выглядел следующим образом:

Таймер до запуска

Таймер до запуска

Экран после нажатия на кнопку «Старт»:

Запущенный таймер

Запущенный таймер

Экран после нажатия на кнопку «Пауза»:

Таймер на паузе

Таймер на паузе

Экран после нажатия кнопки «Стоп»:

Таймер после нажатой кнопки Стоп

Таймер после нажатой кнопки Стоп

Я буду использовать его внешний вид в качестве не строгого референса для своего нового экрана, что слегка упрощает мою задачу. Так же на экране видно, что отображается: название таймера и название звена цепочки таймеров.

Код

Настало время писать код для экрана «Таймер». Я уже реализовал экран «Таймеры», который загружает два демонстрационных варианта: минимальный и максимальный. Теперь моя задача — реализовать воспроизведение этих таймеров. Переход на экран будет осуществляться по нажатию кнопки «Открыть».

timerDML.ts

Первым делом мне нужно написать SQL-запрос в TypeScript, который позволит обратиться к базе данных SQLite3 и получить данные таймера. Получать данные я буду по его id.

export const SELECT_TIMER = 'SELECT * FROM timer WHERE id = ?'

Разбор кода:

  • Объявляется SQL-запрос SELECT_TIMER как строка;

  • Запрос выбирает все поля (SELECT *) из таблицы timer;

  • Используется параметризованный плейсхолдер ? для безопасной подстановки id;

  • Предназначен для получения конкретного таймера по его идентификатору.

timerType.ts

Перед тем как реализовывать логику таймера, важно описать структуру данных и интерфейсы, с которыми будет работать приложение. Это позволит избежать ошибок, упростит разработку и сделает код более предсказуемым. В этом файле я опишу тип таймера, получаемого из базы данных, а также типы, связанные с механизмом обратного отсчёта: входные параметры и возвращаемые значения composable-функции.

import type { Ref, ComputedRef } from 'vue';

// Другой код

/** Полная строка таймера со всеми временными параметрами */
export type TimerRow = {
  id: number;
  title: string;
  pomodoro_time: number;
  break_time: number;
  break_long_time: number;
  count_pomodoro: number;
}

/** Параметры инициализации обратного отсчёта */
export type CountdownOptions = {
  seconds: number;
  onFinish?: () => void;
}

/** Возвращаемые значения и методы composable обратного отсчёта */
export type CountdownReturn = {
  timeLeft: Ref<number>;
  isRunning: Ref<boolean>;
  formatted: ComputedRef<string>; // ЧЧ:ММ:СС
  progress: Ref<number>;
  start: () => void;
  stop: () => void;
  pause: () => void;
}

Разбор кода:

  • Импортируются типы Ref и ComputedRef из Vue для реактивности;

  • TimerRow:

    • Описывает структуру строки из базы данных;

    • Содержит поля таймера: id, название и параметры Pomodoro (время работы, короткого и длинного перерыва, количество циклов).

  • CountdownOptions:

    • Используется при инициализации таймера;

    • seconds — начальное время в секундах;

    • onFinish — необязательный колбэк, вызываемый по завершении таймера.

  • CountdownReturn:

    • Описывает, что возвращает composable обратного отсчёта;

    • timeLeft — оставшееся время (реактивное);

    • isRunning — состояние таймера (запущен или нет);

    • formatted — форматированное время (ЧЧ:ММ:СС);

    • progress — прогресс выполнения таймера;

    • Методы управления: start, stop, pause.

timerCrud.ts

Теперь нужно реализовать слой работы с базой данных, через который я смогу получать конкретный таймер. Несмотря на то что SQL-запрос уже описан ранее, вызывать его напрямую из компонентов — плохая практика. Поэтому я выношу эту логику в отдельную функцию, которая будет инкапсулировать получение таймера по id и возвращать уже готовые данные в нужном формате.

/* Получает таймер по его id **/
export async function getTimer(TimerId: number): Promise<TimerRow> {
  const db = await getDb();
  return await db.select<TimerRow>(SELECT_TIMER, [TimerId]);
}

Разбор кода:

  • Определена асинхронная функция getTimer(TimerId: number);

  • Принимает id таймера в качестве аргумента;

  • Получает подключение к базе данных через getDb();

  • Выполняет SQL-запрос SELECT_TIMER с передачей TimerId в параметры;

  • Использует дженерик <TimerRow> для типизации результата;

  • Возвращает данные одного таймера в виде объекта TimerRow;

  • Инкапсулирует логику работы с БД (удобно для повторного использования и поддержки).

index.ts

После того как данные таймера можно получить из базы, необходимо настроить навигацию к экрану самого таймера. Для этого я добавляю новый маршрут в роутер, который будет принимать id таймера из URL. Это позволит открывать конкретный таймер по нажатию на кнопку «Открыть» и загружать соответствующие данные для отображения и запуска.

Новые строки кода:

import Timer from '@views/Timer/Timer.vue';
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/timers'
  },
  {
    path: '/timers',
    name: 'Timers',
    component: TimersList
  },
  {
    path: '/timer/:id',
    name: 'Timer',
    component: Timer
  }
];

Разбор кода:

  • Импортируется компонент Timer.vue, который отвечает за экран одного таймера;

  • Определяется массив маршрутов routes:

    • / → редирект на /timers (страница по умолчанию);

    • /timers → отображает список таймеров (TimersList);

    • /timer/:id → динамический маршрут для открытия конкретного таймера по id;

  • Параметр :id позволяет получать идентификатор таймера из URL и загружать нужные данные;

  • Связывает навигацию приложения с соответствующими Vue-компонентами.

TimersList.html

После настройки маршрутизации необходимо реализовать переход к конкретному таймеру из списка. Для этого я добавляю кнопку, которая будет формировать ссылку с id выбранного таймера и перенаправлять пользователя на соответствующий экран. Таким образом, каждый элемент списка получает возможность открыть свой таймер для дальнейшей работы с ним.

<button @click="goToTimer(timer.id)" class="btn btn-secondary"> 
  <ion-icon :icon="playCircleOutline"></ion-icon> 
  Открыть 
</button>

Разбор html:

  • Кнопка <button> используется для перехода к выбранному таймеру;

  • Обработчик события @click:

    • Вызывает функцию goToTimer(timer.id) при нажатии;

    • Передаёт id выбранного таймера в качестве аргумента;

    • Навигация реализуется программно (через функцию), а не через ссылку.

  • Внутри кнопки:

    • Иконка (ion-icon) для визуального обозначения действия;

    • Текст "Открыть".

  • По сути: при нажатии вызывается функция, которая выполняет переход на страницу конкретного таймера.

Timer.html

Теперь можно перейти к реализации самого экрана таймера. Здесь отображается вся основная информация: название таймера, текущее время обратного отсчёта и прогресс выполнения. Также на экране размещены кнопки управления (старт, пауза, стоп), которые позволяют управлять состоянием таймера и его воспроизведением. Этот экран объединяет данные из базы и логику обратного отсчёта, формируя полноценный пользовательский интерфейс для работы с таймером.

<ion-page>
  <ion-content>
    <div class="timer-page-container">
      <div class="timer-screen">

        <!-- Timer Card -->
        <div class="timer-display-card">

          <!-- Card top labels -->
          <div class="timer-card-labels">
            <span v-if="timer" class="label-left">{{ timer.title }}</span>
            <span class="label-right">Помидор</span>
          </div>

          <!-- Time display -->
          <div class="timer-time">
            <span class="time-text">{{ countdown?.formatted }}</span>
          </div>

          <!-- Progress bar -->
          <div class="timer-progress-bar">
            <div 
              class="timer-progress-fill" 
              :style="{ width: `${countdown?.progress.value}%` }">
            </div>
          </div>

          <!-- Buttons -->
          <div class="timer-controls">

            <!-- Видна только при running и paused -->
            <button 
              class="btn-timer btn-timer-stop" 
              id="btn-stop"
              v-show="state = 'running' || state = 'paused'"
              @click="() => { setState('idle'); countdown?.stop(); }"
            >
              <ion-icon :icon="stopOutline"></ion-icon>
              Стоп
            </button>

            <!-- Видна при idle и paused -->
            <button 
              class="btn-timer btn-timer-start" 
              id="btn-start"
              v-show="state = 'idle' || state = 'paused'"
              @click="() => { setState('running'); countdown?.start(); }"
            >
              <ion-icon :icon="playOutline"></ion-icon>
              Старт
            </button>

            <!-- Видна только при running -->
            <button 
              class="btn-timer btn-timer-pause" 
              id="btn-pause"
              v-show="state = 'running'"
              @click="() => { setState('paused'); countdown?.pause(); }"
            >
              <ion-icon :icon="pauseOutline"></ion-icon>
              Пауза
            </button>

          </div>
        </div>

      </div>
    </div>
  </ion-content>
</ion-page>

Основные блоки интерфейса:

  • Обёртка страницы:

    • <ion-page> и <ion-content> — базовая структура Ionic-страницы.

  • Контейнер таймера:

    • .timer-page-container и .timer-screen — layout и центрирование.

  • Карточка таймера (timer-display-card):

    • Основной визуальный блок с таймером.

  • Верхние лейблы (timer-card-labels):

    • Слева — название таймера (timer.title, если есть данные);

    • Справа — текущий режим ("Помидор").

  • Отображение времени (timer-time):

    • Показывает отформатированное время (countdown.formatted);

    • Основной элемент UI (что видит пользователь).

  • Прогресс-бар (timer-progress-bar):

    • Внутренний блок (timer-progress-fill);

    • Ширина задаётся динамически через :style;

    • Использует countdown.progress.value → отображает % выполнения.

  • Кнопки управления (timer-controls):

    • Стоп

      • Видна при running и paused;

      • Останавливает таймер (countdown.stop()) и сбрасывает состояние.

    • Старт

      • Видна при idle и paused;

      • Запускает таймер (countdown.start()).

    • Пауза

      • Видна только при running;

      • Приостанавливает таймер (countdown.pause()).

Vue-логика и реактивность:

  • v-if="timer":

    • Условный рендеринг — показывает данные только когда таймер загружен.

  • v-show:

    • Управляет видимостью кнопок без удаления из DOM;

    • Основан на состоянии state (idle, running, paused).

  • Реактивные данные (countdown):

    • formatted — автоматически обновляет отображение времени;

    • progress.value — влияет на ширину прогресс-бара;

    • При изменении значений UI обновляется без перерендера страницы.

  • События (@click):

    • Каждая кнопка вызывает:

      • изменение состояния (setState);

      • метод таймера (start, stop, pause).

    • Используются inline-функции.

  • Динамические привязки:

    • :style — реактивно меняет CSS (ширину прогресса);

    • :icon — подставляет нужную иконку.

  • Общий принцип:

    • UI полностью зависит от состояния (state) и реактивных данных (countdown);

    • При изменении состояния автоматически:

      • меняются кнопки;

      • обновляется время;

      • двигается прогресс-бар.

Если упростить: это реактивный таймер, где Vue синхронизирует состояние (логика) и интерфейс (DOM) без ручного обновления.


Timer.css

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

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.timer-page-container {
  background-color: #f5f5f5;
  min-height: 100vh;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
    'Helvetica Neue', Arial, sans-serif;
  display: flex;
  align-items: center;
  justify-content: center;
}

.timer-screen {
  padding: 24px 16px;
  width: 100%;
  max-width: 500px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.timer-display-card {
  background-color: #ffffff;
  border-radius: 16px;
  padding: 28px 24px 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 20px;
  width: 100%;
}

.timer-card-labels {
  display: flex;
  justify-content: space-between;
  width: 100%;
}

.label-left,
.label-right {
  font-size: 13px;
  color: #9e9e9e;
  font-weight: 400;
  letter-spacing: 0.3px;
}

.timer-time {
  width: 100%;
  text-align: center;
}

.time-text {
  font-size: 56px;
  font-weight: 400;
  color: #212121;
  letter-spacing: 2px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}

.timer-progress-bar {
  width: 100%;
  height: 2px;
  background-color: #e0e0e0;
  border-radius: 2px;
  overflow: hidden;
}

.timer-progress-fill {
  height: 100%;
  background-color: #1976d2;
  border-radius: 2px;
  transition: width 1s linear;
}

.timer-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  height: 48px;
}

.btn-timer {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 120px;
  height: 44px;
  flex-shrink: 0; /* запрет на сжатие */
  background-color: transparent;
  font-size: 14px;
  font-weight: 500;
  border-radius: 50px;
  cursor: pointer;
  letter-spacing: 0.5px;
  border: 2px solid;
}

.btn-timer ion-icon {
  font-size: 17px;
}

.btn-timer-start {
  border-color: #4caf50;
  color: #4caf50;
}

.btn-timer-start:hover {
  background-color: rgba(76, 175, 80, 0.10);
}

.btn-timer-start:active {
  background-color: rgba(76, 175, 80, 0.22);
}

.btn-timer-stop {
  border-color: #f44336;
  color: #f44336;
}

.btn-timer-stop:hover {
  background-color: rgba(244, 67, 54, 0.10);
}

.btn-timer-stop:active {
  background-color: rgba(244, 67, 54, 0.22);
}

.btn-timer-pause {
  border-color: #ffc107;
  color: #f9a825;
}

.btn-timer-pause:hover {
  background-color: rgba(255, 193, 7, 0.10);
}

.btn-timer-pause:active {
  background-color: rgba(255, 193, 7, 0.22);
}

.btn-timer {
  transition: all 0.2s ease, opacity 0.25s ease, transform 0.25s ease;
}

.btn-timer.hidden {
  display: none;
}

@media (max-width: 600px) {
  .timer-display-card {
    padding: 20px 16px;
    border-radius: 12px;
    gap: 16px;
  }

  .time-text {
    font-size: 44px;
  }

  .btn-timer {
    padding: 8px 16px;
    font-size: 13px;
  }
}

Краткий разбор css:

* — применяет стили ко всем элементам, margin — внешний отступ, padding — внутренний отступ, box-sizing: border-box — включает padding и border в размеры элемента.

  • .timer-page-container — задаёт фон, шрифт и центрирует всё через flex;

  • .timer-display-card — белая карточка с border-radius, box-shadow, отступами;

  • .time-text (внутри .timer-time) — крупный текст времени по центру;

  • .timer-progress-bar и .timer-progress-fill — линия прогресса с плавным transition ширины;

  • .btn-timer, .btn-timer-start/stop/pause — кнопки с цветами, рамками и hover/active эффектами;

  • @media (max-width: 600px) — уменьшает размеры элементов для мобильных устройств.

useTimer.ts

После настройки интерфейса и получения данных таймера я хочу реализовать сам механизм обратного отсчёта. Чтобы не смешивать логику с компонентом, я выношу её в отдельный composable. Такой подход позволяет переиспользовать таймер, упростить тестирование и сделать код более читаемым. В этом файле будет описана логика управления временем: запуск, пауза, остановка, форматирование отображения и расчёт прогресса выполнения таймера.

import { ref, computed } from 'vue';
import { CountdownOptions, CountdownReturn } from '@/types/timerType';

/** Composable для управления обратным отсчётом времени */
export function useCountdown({ seconds, onFinish }: CountdownOptions): CountdownReturn {
  const totalSeconds = seconds * 60;

  /** Реактивное оставшееся время в секундах */
  const timeLeft = ref<number>(totalSeconds);

  /** Флаг активного состояния таймера */
  const isRunning = ref<boolean>(false);

  let interval: ReturnType<typeof setInterval> | null = null;

  /** Форматирует оставшееся время в строку ЧЧ:ММ:СС */
  const formatted = computed<string>(() => {
    const hours = Math.floor(timeLeft.value / 3600);
    const mins = Math.floor((timeLeft.value % 3600) / 60);
    const secs = timeLeft.value % 60; 
    return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
  });

  /** Вычисляет процент прошедшего времени от общего */
  const progress = computed<number>(() => {
    return ((totalSeconds - timeLeft.value) / totalSeconds) * 100;
  });

  /** Запускает таймер */
  const start = (): void => {
    if (isRunning.value) return
    isRunning.value = true;

    interval = setInterval(() => {
      if (timeLeft.value > 0) {
        timeLeft.value--;
      } else {
        stop();
        onFinish?.();
      }
    }, 1000);
  }

  /** Останавливает таймер и сбрасывает время до начального */
  const stop = (): void => {
    if (interval) {
      clearInterval(interval);
      interval = null;
    }
    isRunning.value = false;
    timeLeft.value = totalSeconds;
  }

  /** Приостанавливает таймер без сброса оставшегося времени */
  const pause = (): void => {
    if (!isRunning.value) return
    if (interval) {
      clearInterval(interval);
      interval = null;
    }
    isRunning.value = false;
  }

  return { 
    timeLeft, 
    isRunning, 
    formatted,
    progress, 
    start, 
    stop, 
    pause
  };
}

Краткий разбор кода:

  • useCountdown — composable для управления таймером обратного отсчёта;

  • seconds * 60 — перевод минут в секунды и сохранение общего времени;

  • timeLeft (ref) — реактивное текущее оставшееся время;

  • isRunning (ref) — флаг: таймер запущен или нет;

  • interval — хранит setInterval, чтобы можно было остановить таймер;

  • formatted (computed) — форматирует секунды в строку ЧЧ:ММ:СС;

  • progress (computed) — считает процент выполнения таймера;

  • start() — запускает таймер через setInterval, уменьшает время каждую секунду, вызывает onFinish при завершении;

  • stop() — полностью останавливает таймер и сбрасывает время к начальному значению;

  • pause() — останавливает таймер, но сохраняет текущее оставшееся время;

  • return — отдаёт наружу все состояния и методы для использования в компоненте.

Timer.ts

Теперь необходимо связать всё вместе: данные из базы, логику таймера и пользовательский интерфейс. В этом компоненте я реализую экран таймера, который загружает данные по id из маршрута, инициализирует обратный отсчёт и управляет состоянием кнопок (старт, пауза, стоп). По сути, это точка, где объединяются все ранее написанные части — работа с БД, composable с таймером и отображение на экране.

import { defineComponent, onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { IonIcon, IonPage, IonContent } from '@ionic/vue';
import { useRoute } from 'vue-router';

import { 
  stopOutline, 
  playOutline, 
  pauseOutline 
} from 'ionicons/icons';

import { getTimer } from '@/db/crud/timerCrud';
import type { TimerRow } from '@/types/timerType';
import { useCountdown } from '@/composables/useTimer';
import { CountdownReturn } from '@/types/timerType';

/** Возможные состояния таймера */
type TimerState = 'idle' | 'running' | 'paused';

/** Компонент страницы таймера с управлением обратным отсчётом */
export default defineComponent({
  name: 'Timer',
  components: { IonIcon, IonPage, IonContent },

  /** Инициализирует данные и логику компонента таймера */
  setup() {
    const route = useRoute();
    const id = Number(route.params.id);

    /** Реактивные данные загруженного таймера */
    const timer = ref<TimerRow>();

    /** Сообщение об ошибке при загрузке */
    const error = ref<string | null>(null);

    /** Текущее состояние кнопок управления */
    const state = ref<TimerState>('idle');

    /** Обновляет текущее состояние таймера */
    const setState = (newState: TimerState) => {
      state.value = newState;
    };

    /** Shallow-ссылка на экземпляр composable обратного отсчёта */
    const countdown = shallowRef<CountdownReturn | null>(null);

    /** Останавливает таймер при размонтировании компонента */
    onUnmounted(() => {
      countdown.value?.stop();
    });

    /** Загружает данные таймера из БД и инициализирует отсчёт */
    const loadTimer = async (): Promise<void> => {
      try {
        const result = await getTimer(id);
        const timerData = Array.isArray(result) ? result[0] : result;
        timer.value = timerData;

        countdown.value = useCountdown({
          seconds: timerData.pomodoro_time ?? 0,
          onFinish: () => alert('Время вышло!')
        });

      } catch (e) {
        error.value = 'Ошибка загрузки таймера';
        console.error(e);
      }
    };

    onMounted(loadTimer);

    return {
      timer,
      error,
      state,
      setState,
      stopOutline,
      playOutline,
      pauseOutline,
      countdown
    };
  }
});

Разбор кода:

  • Инициализация компонента (defineComponent):

    • Создаёт Vue-компонент страницы таймера в Ionic.

  • Получение параметров маршрута (useRoute):

    • Берёт id из URL (/timer/:id) для загрузки нужного таймера.

  • Состояние данных:

    • timer (ref) — данные таймера из базы (название, время и т.д.);

    • error (ref) — сообщение об ошибке загрузки;

    • state (ref) — состояние таймера (idle, running, paused).

  • Управление состоянием:

    • setState() — переключает текущее состояние таймера (логика кнопок и UI).

  • Обратный отсчёт (useCountdown):

    • countdown (shallowRef) — экземпляр таймера;

    • Создаётся после загрузки данных из БД;

    • Использует pomodoro_time как начальное время;

    • onFinish — показывает уведомление при окончании.

  • Загрузка данных (loadTimer):

    • Вызывает getTimer(id) из базы данных;

    • Обрабатывает результат (массив или объект);

    • Инициализирует timer и countdown.

  • Жизненный цикл Vue:

    • onMounted(loadTimer) — загружает таймер при открытии страницы;

    • onUnmounted() — останавливает таймер при выходе со страницы.

  • Возвращаемые значения в шаблон:

    • timer — данные таймера;

    • error — ошибка загрузки;

    • state и setState — управление UI и кнопками;

    • countdown — методы и данные таймера (start/stop/pause, progress, formatted);

    • stopOutline / playOutline / pauseOutline — иконки Ionic.

Timer.vue

После того как разметка, стили и логика таймера реализованы, нужно объединить их в один компонент. В этом файле я подключаю отдельные части (html, ts, css), чтобы сформировать полноценную страницу таймера. Такой подход позволяет держать код структурированным и упрощает поддержку, так как каждая часть отвечает только за свою зону ответственности.

<!-- Timer.vue -->
<template src="@/views/Timer/Timer.html"></template>
<script lang="ts" src="@/views/Timer/Timer.ts"></script>
<style src="@/views/Timer/Timer.css" scoped></style>

Разбор кода:

  • <template> — подключает HTML-разметку таймера;

  • <script> — подключает логику (Timer.ts);

  • <style scoped> — стили применяются только к этому компоненту.


Проверка работы приложения

Когда вся функциональность экрана таймера реализована, остаётся проверить, как всё работает вместе. Для этого я запускаю приложение в режиме разработки, чтобы убедиться, что таймер корректно загружается, отображается и управляется через интерфейс.

cargo tauri dev

На экране «Таймеры» выберу таймер с названием Timer mini example и перейду к нему, нажав на кнопку «Открыть».

Экран Таймеры

Экран Таймеры

Открылся экран «Таймер», который напоминает таймер, реализованный мной ранее на Kivy. Главным отличием является реализация полоски прогресса прошедшего и оставшегося времени.

Слева отображается название таймера, а справа — название цепочки «Помидор», которое пока захардкожено в html, так как механика цепочки ещё не реализована.

Для запуска таймера нажимаю на кнопку «Старт».

Таймер до запуска

Таймер до запуска

После запуска таймера видно, как начался обратный отсчёт и заполняется прогресс-бар. Также исчезла кнопка «Старт» и появились две кнопки: «Стоп» и «Пауза». Теперь нажму на кнопку «Пауза».

Запущенный таймер

Запущенный таймер

После нажатия на кнопку «Пауза» появилась кнопка «Старт», с помощью которой можно продолжить воспроизведение. Однако я нажму на кнопку «Стоп» для полного сброса и остановки таймера.

Таймер на паузе

Таймер на паузе

После нажатия на кнопку «Стоп» таймер вернулся в исходное состояние перед запуском. Его можно снова запустить, что я и сделаю для проверки работы прогресс-бара.

Таймер сброшенный кнопкой Стоп

Таймер сброшенный кнопкой Стоп

Прошло 5 минут из 10, и линия прогресс-бара визуально заполнилась ровно наполовину. Это говорит о том, что расчёты логики выполнены корректно.

Прогресс бар 50%

Прогресс бар 50%

Что произойдёт после того, как прогресс-бар полностью заполнится, а время дойдёт до 00:00:00? Сейчас там используется функция alert в качестве заглушки, которая сообщает, что время вышло. После нажатия на «Close» таймер снова становится готов к запуску.

Таймер сработал

Таймер сработал

Пожалуй, это вся функциональность, которую я реализовал в рамках этой статьи. На данный момент это примерно половина реализации экрана «Таймер».


Анонс на следующие статьи

В следующей статье я продолжу разрабатывать функциональность экрана «Таймер». Мне предстоит реализовать механизм цепочки таймеров, которые будут воспроизводиться друг за другом. В эту цепочку войдут: помидоры, короткие перерывы и один длительный перерыв.

Также планирую добавить кнопку «Назад» в виде стрелки, которая позволит вернуться к списку таймеров.

В завершение хочу реализовать звуковое оповещение. Для этого нужно разобраться с воспроизведением звука средствами браузерного движка (Web Audio / HTMLAudioElement). Такой подход позволит одинаково удобно воспроизводить звук на разных платформах, так как Tauri использует встроенный веб-движок системы.

Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!

Читайте продолжение — не пропустите!


Ссылки к статье