Вступление
Всем доброго дня! В предыдущей статье Kawai-Focus 2.5: сборка и упаковка Tauri-приложения (Windows + Arch Linux, AUR):
Собрано приложение под Windows;
Отправлен рецепт в 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, и линия прогресс-бара визуально заполнилась ровно наполовину. Это говорит о том, что расчёты логики выполнены корректно.

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

Пожалуй, это вся функциональность, которую я реализовал в рамках этой статьи. На данный момент это примерно половина реализации экрана «Таймер».
Анонс на следующие статьи
В следующей статье я продолжу разрабатывать функциональность экрана «Таймер». Мне предстоит реализовать механизм цепочки таймеров, которые будут воспроизводиться друг за другом. В эту цепочку войдут: помидоры, короткие перерывы и один длительный перерыв.
Также планирую добавить кнопку «Назад» в виде стрелки, которая позволит вернуться к списку таймеров.
В завершение хочу реализовать звуковое оповещение. Для этого нужно разобраться с воспроизведением звука средствами браузерного движка (Web Audio / HTMLAudioElement). Такой подход позволит одинаково удобно воспроизводить звук на разных платформах, так как Tauri использует встроенный веб-движок системы.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Ссылки к статье
Репозиторий проекта на Github Kawai-Focus-v2.