Всем доброго дня! В предыдущей статье Kawai-Focus 2.2: Python-бинарник в Tauri — проблемы и альтернативы:

  1. Освещены неработающие моменты с бинарником на Arch Linux;

  2. Рассмотрены альтернативы, которые могут исправить проблемы с бинарником;

  3. Внедрён оптимальный (для меня) вариант, который исправил половину неисправностей.

В данной статье я покажу код на JS, который не поместился в предыдущей статье, а также перепишу его на TS. Кратко расскажу о преимуществах TS над JS и о том, что необходимо понимать для перехода.

В прошлой статье я также упоминал, что у Сергея получилось запустить мой проект на Tauri в режиме разработки на Arch. Он поделился со мной информацией в issue на GitHub и тем самым внёс вклад в проект. Поэтому я решил попробовать исправить проблему на основе его issue. Заодно расскажу, что такое issue и как оно выглядит.

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

Логика приложения на TS

Как я писал в предыдущей статье, логика, которую я реализовал на JS, не поместилась в неё полностью, поэтому я решил подробнее рассказать об этом коде здесь. Ранее я уже имел дело с чистым JS, хоть и не очень много, поэтому написание прототипа не оказалось для меня сложной задачей.

Однако чем больше кода я писал, тем сильнее осознавал, что чистый JS подходит в основном для относительно простых задач. Например, в нём отсутствует строгая типизация и ряд возможностей, к которым я привык при работе с Python. В прошлой статье я использовал JS, чтобы ускорить разработку, поскольку параллельно было много других задач. Освоение TS тогда заняло бы слишком много времени — для меня он был «тёмным лесом».

К счастью, сейчас у меня появилось больше времени TS, чтобы улучшить текущую реализацию и при этом переписав минимальное количество кода.

Почему минимум?

Потому что TypeScript — это надстройка над JavaScript. Он не заменяет JS, а расширяет его. Большая часть уже написанного кода остаётся валидной и продолжает работать без изменений. В большинстве случаев переход начинается с простого добавления типов к переменным, функциям и возвращаемым значениям.

Кроме того, TypeScript компилируется в обычный JavaScript, поэтому логика приложения остаётся прежней — меняется лишь уровень контроля на этапе разработки. Фактически, разработчик не переписывает архитектуру, а постепенно усиливает её типами. Это особенно удобно, когда проект уже рабочий: можно внедрять TS поэтапно, начиная с отдельных файлов.

Главное преимущество TS перед JS — возможность находить ошибки ещё до запуска программы, улучшенная читаемость и поддерживаемость кода, а также более удобная работа в больших проектах благодаря автодополнению и строгой структуре типов.

Установка необходимых плагинов

Первым делом мне нужно установить необходимые для работы c Tauri через JS/TS плагины, а лишние, которые ещё остались со времен Python бинарника удалить.

Перехожу в папку client:

cd client

Для начала удалю @tauri-apps/plugin-shell , который позволяет вызывать системные команды и запускать внешние процессы (например, Python-бинарник) из приложения, управляя ими через безопасный интерфейс между фронтендом и нативной частью.

npm uninstall @tauri-apps/plugin-shell

Теперь установлю пару плагинов, которые мне пригодятся для работы.

npm install @tauri-apps/plugin-sql @tauri-apps/api

Разбор новых плагинов:

  • @tauri-apps/plugin-sql — плагин Tauri, который добавляет в приложение доступ к SQL‑базам (например, SQLite) через единый API для выполнения запросов и работы с по��ключениями;

  • @tauri-apps/api — основной клиентский пакет JS/TS с API Tauri для взаимодействия фронтенда с “нативной” частью (окна, файловая система, диалоги, события, invoke к Rust-командам и т.п.).

Работа с базой данных

Первым делом я написал скрипт, который будет создавать базу данных при старте приложения, если он не обнаружит её в указанном каталоге. Логика должна быть максимально разделена, чтобы при необходимости можно было заменить CRUD-подход с JS/TS на подход через API для веб-версии, изменив при этом минимум кода.

Плагин tauri-plugin-sql работает с базой данных не через ORM-подход, а через SQL-запросы, которые программист вставляет в нужные функции в виде строк. Если база данных небольшая и состоит из одной таблицы timer, как в моём случае, то SQL-подход вполне подойдёт. Кроме того плагин довольно лёгкий, что даёт плюс по ресурсам когда проект небольшой.

Когда-то давно мне доводилось писать прототип базы данных MySQL на чистых DML- и DDL-запросах. Кроме того, в некоторых компаниях этот навык требуется, поэтому его использование будет полезно для освежения знаний SQL.

DML (Data Manipulation Language) — это группа SQL-операторов для работы с данными в таблицах: выборка, вставка, обновление и удаление (например, SELECT, INSERT, UPDATE, DELETE).

DDL (Data Definition Language) — это группа SQL-операторов для описания и изменения структуры объектов базы данных (например, CREATE, ALTER, DROP).

timerDDL.js

Сначала я создаю файл client/db/ddl/timerDDL.js для формирования SQL-запроса на создание базы данных. Поскольку база данных у меня уже была создана ранее, я могу посмотреть схему таблицы timer в виде DDL-запроса в VS Code, открыв её через плагин SQLite3 Editor.

схема таблицы user
схема таблицы user

timerDDL.js

export const CREATE_TIMER = `
  PRAGMA foreign_keys = ON;
  CREATE TABLE IF NOT EXISTS timer (
    title VARCHAR(200) NOT NULL,
    pomodoro_time INTEGER NOT NULL,
    break_time INTEGER NOT NULL,
    break_long_time INTEGER NOT NULL,
    count_pomodoro INTEGER NOT NULL,
    id INTEGER NOT NULL,
    PRIMARY KEY (id)
  );
`;

Разбор кода:

  • export const CREATE_TIMER — экспортируемая JavaScript константа для SQL-запроса;

  • Обратные кавычки (backticks) — шаблонная строка (template literal) для многострочного SQL;

  • PRAGMA foreign_keys = ON; — включает поддержку внешних ключей в SQLite (иначе они игнорируются);

  • CREATE TABLE IF NOT EXISTS — создаёт таблицу timer, только если её ещё нет (безопасно при повторном выполнении);

  • title VARCHAR(200) NOT NULL — название таймера, до 200 символов, обязательно;

  • pomodoro_time INTEGER NOT NULL — длительность одного помидоро в минутах;

  • break_time INTEGER NOT NULL — длительность короткого перерыва;

  • break_long_time INTEGER NOT NULL — длительность длинного перерыва;

  • count_pomodoro INTEGER NOT NULL — количество помидоро в цикле до длинного перерыва;

  • id INTEGER NOT NULL — уникальный идентификатор таймера;

  • PRIMARY KEY (id) — id является первичным ключом (уникальный, индексированный);

  • Точка с запятой в конце — корректное завершение SQL-скрипта.

Данный .js-файл хранит простую переменную со строкой, поэтому для преобразования в TypeScript достаточно переименовать его расширение с .js на .ts.

timerDML.js

Далее я создал client/src/db/dml/timerDML.js для работы с таблицей timer.

export const SELECT_TIMERS = 'SELECT id, title, pomodoro_time, count_pomodoro FROM timer ORDER BY id DESC'
export const COUNT_TIMERS = 'SELECT COUNT(*) as cnt FROM timer'
export const INSERT_SEED_DB = `
INSERT INTO timer (title, pomodoro_time, break_time, break_long_time, count_pomodoro) VALUES
('Timer mini example', 10, 3, 15, 2), ('Timer max example', 90, 10, 40, 8)
`

SELECT_TIMERS:

  • SELECT id, title, pomodoro_time, count_pomodoro — выбирает ключевые поля для отображения;

  • FROM timer — из таблицы таймеров;

  • ORDER BY id DESC — сортировка по ID по убыванию (новые таймеры сверху).

COUNT_TIMERS:

  • SELECT COUNT(*) — подсчёт общего количества записей в таблице;

  • as cnt — псевдоним результата ({ cnt: 5 });

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

INSERT_SEED_DB:

  • INSERT INTO timer (...) — вставка начальных данных (seed);

  • Множественная вставка — 2 записи за один запрос (эффективнее);

  • ('Timer mini example', 10, 3, 15, 2) — короткий таймер: 10 мин работа, 3 мин перерыв, 15 мин длинный, 2 помидоро на цикл;

  • ('Timer max example', 90, 10, 40, 8) — длинный таймер: 90 мин работа, 10 мин перерыв, 40 мин длинный, 8 помидоро на цикл.

  • id не указан — автоинкремент (PRIMARY KEY).

Здесь я, аналогично предыдущему файлу, переименовал расширение с .js на .ts.

config.js

После того как я создал SQL-запросы в виде строк, я приступил к реализации механизма, который будет их использовать. Чтобы добраться до самих запросов, необходимо сначала подключиться к базе данных, для чего нужно сформировать url. Для этого я создал файл client/src/config.js, в котором реализовал функцию getDB_URL().

import { appLocalDataDir } from '@tauri-apps/api/path';

export async function getDB_URL() {

  const appDir = await appLocalDataDir();
  return `sqlite:${appDir}/timer.db`;
}

Разбор кода:

  • import { appLocalDataDir } — импортирует API Tauri для получения локальной папки приложения;

  • export async function getDB_URL() — асинхронная функция, во��вращающая URL базы данных;

  • await appLocalDataDir() — получает путь к папке данных приложения:

    • WindowsC:\Users<user>\AppData\Local<appname>;

    • macOS~/Library/Application Support/<appname>;

    • Linux~/.local/share/<appname>.

  • sqlite:${appDir}/timer.db — формирует URI для SQLite-плагина Tauri:

    • sqlite: — схема протокола для @tauri-apps/plugin-sql;

    • ${appDir}/timer.db — путь к файлу timer.db в папке приложения.

  • Результат"sqlite:/home/user/.local/share/kawai-focus/timer.db"

В данной функции не хватает аннотации возвращаемого значения и строки документации.

import { appLocalDataDir } from '@tauri-apps/api/path';

/** Возвращает URL подключения к SQLite */
export async function getDB_URL(): Promise<string> {
  const appDir = await appLocalDataDir();
  return `sqlite:${appDir}/timer.db`;
}

Promise<string> в TypeScript — это тип, который означает, что функция возвращает промис — объект, представляющий результат асинхронной операции.

Промис может находиться в трёх состояниях:

  • ожидание (pending);

  • выполнен успешно (fulfilled / resolve);

  • завершён с ошибкой (rejected / reject).

Если промис завершается успешно, то в случае Promise<string> он вернёт значение типа string. Это даёт статическую проверку типов и гарантирует, что результат асинхронной операции будет именно строкой, а не числом, объектом или undefined.

seed.ts

Когда запускается приложение Kawai-Focus, оно должно заполнить базу демонстрационными данными. В моём случае — двумя демонстрационными таймерами в таблице timer. Это должно происходить только в том случае, если база данных пуста. Для этого я создал файл client/src/db/seed.ts, в котором будет находиться функция seedDb() для заполнения данных.

Думаю, с .js-файлами я уже привёл достаточно примеров, а основное отличие заключается в использовании типов в TS. Поэтому далее я буду сразу показывать .ts-файлы.

Перед тем как перейти к коду функции seedDb(), я создам для неё тип, который поможет с валидацией. В данном случае мне потребуется валидация количества записей в таблице. Для этого я создам файл client/src/types/timerType.ts.

timerType.ts

export type CountRow = { cnt: number; };

Разбор кода:

  • export — делает тип доступным для использования в других файлах проекта;

  • type — объявляет пользовательский тип в TypeScript;

  • CountRow — имя типа, которое описывает структуру строки результата SQL-запроса;

  • { cnt: number; } — объектный тип с одним свойством:

    • cnt — поле, соответствующее алиасу в SQL-запросе (например, SELECT COUNT(*) as cnt);

    • number — тип значения (число);

  • Назначение типа — гарантирует, что результат запроса COUNT_TIMERS будет содержать поле cnt именно числового типа, что позволяет избежать ошибок при обращении к нему;

seed.ts

import Database from '@tauri-apps/plugin-sql';
import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML';
import { CountRow } from '../types/timerType';

/** Заполняет бд данными (демо таймерами) */
export async function seedDb(db: Database) {
  const count = await db.select<CountRow[]>(COUNT_TIMERS);
  const cnt = count[0]?.cnt ?? 0;

  if (cnt = 0) {
    await db.execute(INSERT_SEED_DB);
  }
}

Разбор кода:

  • import Database from '@tauri-apps/plugin-sql' — импортирует класс Database из плагина Tauri для работы с SQL-базой данных (SQLite, MySQL и др. в зависимости от конфигурации);

  • import { INSERT_SEED_DB, COUNT_TIMERS } — импортирует SQL-запросы:

    • COUNT_TIMERS — запрос для получения количества записей в таблице timer;

    • INSERT_SEED_DB — запрос для вставки демонстрационных таймеров;

  • import { CountRow } — импортирует TypeScript-тип, описывающий структуру строки результата запроса подсчёта (например, { cnt: number });

  • export async function seedDb(db: Database) — асинхронная функция, которая принимает подключение к базе данных и выполняет начальное заполнение таблицы;

  • await db.select<CountRow[]>(COUNT_TIMERS) — выполняет SQL-запрос на выборку:

    • <CountRow[]> — указывает тип возвращаемых данных (массив строк результата);

    • результатом будет массив объектов, соответствующих структуре CountRow;

  • const cnt = count[0]?.cnt ?? 0;:

    • count[0]?.cnt — безопасно получает значение поля cnt из первой строки результата;

    • ?. — optional chaining (защита от undefined);

    • ?? 0 — если значение отсутствует, используется 0 по умолчанию;

  • if (cnt = 0) — проверяет, пуста ли таблица timer;

  • await db.execute(INSERT_SEED_DB); — если таблица пуста, выполняет SQL-запрос вставки демо-данных;

  • Результат: при первом запуске приложение автоматически заполняет базу демонстрационными таймерами, а при повторных запусках данные не дублируются.

tsconfig.json

Вы, наверное, уже обратили внимание на неудобные относительные пути в импортах вроде './dml/timerDML', которые режут глаза и сильно неудобны. Для упрощения написания импортов можно указать в client/src/tsconfig.json, чтобы корень приложения ассоциировался с @/. Таким образом не нужно будет прыгать по относительным путям туда-сюда.

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "module": "ESNext",
    "target": "ES2020",
    "moduleResolution": "Bundler"
  }
}

Разбор конфига:

  • baseUrl: "." — задаёт базовую директорию проекта для TypeScript, от которой будут считаться пути;

  • paths — позволяет создать алиасы для импортов:

    • "@/*": ["src/*"]@/ теперь соответствует папке src, что сокращает и упрощает импорты;

  • module: "ESNext" — указывает формат модулей (поддержка современных ES-модулей);

  • target: "ES2020" — определяет версию JavaScript, в которую будет транслироваться код;

  • moduleResolution: "Bundler" — указывает, как TypeScript будет разрешать пути модулей, оптимально для сборщиков вроде Vite или Webpack.

Старые импорты в seed.ts:

import { INSERT_SEED_DB, COUNT_TIMERS } from './dml/timerDML';
import { CountRow } from '../types/timerType'

Новые импорты в seed.ts:

import { INSERT_SEED_DB, COUNT_TIMERS } from '@/db/dml/timerDML'
import { CountRow } from '@/types/timerType'

Теперь импорты стали намного удобнее и читаемее.

initDb.ts

Далее мне понадобилась функция getDb(), которая инициализирует базу данных и возвращает подключение к ней. Для этого я создал файл client/src/db/initDb.ts.

import Database from '@tauri-apps/plugin-sql';
import { CREATE_TIMER } from '@/db/ddl/timerDDL';
import { getDB_URL } from '@/config';
import { seedDb } from '@/db/seed';

let dbPromise: Promise<Database> | null = null;

/** Получает подключение к бд */
export async function getDb(): Promise<Database> {
  if (!dbPromise) {
    dbPromise = (async () => {
      try {
        const dbUrl = await getDB_URL();
        const db = await Database.load(dbUrl);
        await db.execute(CREATE_TIMER);
        await seedDb(db);
        return db;
      } catch (error) {
        console.error('Ошибка инициализации базы данных', error);
        throw error;
      }
    })();
  }

  return await dbPromise;
}

Функция getDb()

  • Асинхронная функция, которая возвращает подключение к базе данных (Database);

  • Использует паттерн singleton: создаётся один экземпляр подключения и повторно используется при последующих вызовах;

  • Основные шаги при первом вызове:

    1. Получает URL базы данных через getDB_URL();

    2. Загружает базу данных с помощью Database.load(dbUrl);

    3. Создаёт таблицу timer, если она ещё не существует (CREATE_TIMER);

    4. Заполняет таблицу демонстрационными данными через seedDb(db).

  • В случае ошибки логирует её в консоль и пробрасывает дальше;

  • При повторных вызовах возвращает уже готовое подключение, не создавая новый экземпляр.

timerCrud.ts

Наконец я дошёл до первой CRUD-операции, которая является частью функционала приложения. Для этого нужно реализовать select-операцию, которая получает список таймеров. CRUD-операции для таймеров будут находиться в client/src/db/crud/timerCrud.ts.

Для проверки данных, которые функция будет получать из базы данных, я создал тип TimersRow.

timerType.ts

export type TimersRow = {
  id: number;
  title: string;
  pomodoro_time: number;
  count_pomodoro: number;
};

timerCrud.ts

import { getDb } from "@/db/initDb";
import { SELECT_TIMERS } from "@/db/dml/timerDML";
import { TimersRow } from "@/types/timerType"

/** Получает список таймеров */
export async function getTimers(): Promise<TimersRow[]> {
  const db = await getDb();
  return await db.select<TimersRow[]>(SELECT_TIMERS);
}

Функции getTimers():

  • Асинхронная функция, которая возвращает список всех таймеров из базы данных;

  • Подключается к базе через getDb();

  • Выполняет SQL-запрос SELECT_TIMERS и возвращает результат в виде массива объектов типа TimersRow;

  • Возвращаемый тип: Promise<TimersRow[]>, что гарантирует корректность данных через TypeScript.

Этого достаточно, чтобы начать работать с базой данных. Единственное, чего я пока не реализовал, — это работа с миграциями. Хорошая новость в том, что tauri-plugin-sql их поддерживает. Я обязательно внедрю в этот проект миграции, которые позволят удобно расширять базу данных по мере необходимости, но сделаю это в одной из следующих статей.


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

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

Данная статья не о HTML и CSS, поэтому я не буду на них останавливаться, а уделю внимание логике на TypeScript и Vue.

TimersList.ts

Я написал файл client/src/views/TimersList.ts, который необходим для логики работы экрана Таймеры.

import { defineComponent, onMounted, ref } from 'vue'
import { IonIcon } from '@ionic/vue'
import {
  timerOutline,
  chevronUp,
  chevronDown,
  playCircleOutline,
  pencilOutline,
  trashOutline,
} from 'ionicons/icons'

import { getTimers } from '@/db/crud/timerCrud'
import type { TimersRow } from '@/types/timerType'

export default defineComponent({
  name: 'TimersList',
  components: { IonIcon },
  setup() {
    const timers = ref<TimersRow[]>([])
    const loading = ref<boolean>(true)
    const error = ref<string | null>(null)
    const expandedId = ref<number | null>(null)

    const loadTimers = async (): Promise<void> => {
      loading.value = true
      error.value = null

      try {
        const result = await getTimers()
        timers.value = result
      } catch (e: unknown) {
        error.value = e instanceof Error ? e.message : 'Ошибка загрузки таймеров'
      } finally {
        loading.value = false
      }
    }

    const toggleExpand = (id: number): void => {
      expandedId.value = expandedId.value = id ? null : id
    }

    onMounted(loadTimers)

    return {
      timers,
      loading,
      error,
      expandedId,

      toggleExpand,

      timerOutline,
      chevronUp,
      chevronDown,
      playCircleOutline,
      pencilOutline,
      trashOutline,
    }
  },
})

Разбор TimersList.vue:

  • Это Vue 3 компонент, написанный с использованием Composition API.

  • Импортирует иконки из Ionicons (IonIcon) для отображения интерфейса.

  • Получает данные из базы через функцию getTimers() из timerCrud.

Основные части:

  1. Состояния (ref):

    • timers — массив таймеров (TimersRow[]);

    • loading — индикатор загрузки;

    • error — текст ошибки (если загрузка не удалась);

    • expandedId — id таймера, который в данный момент раскрыт (для UI).

  2. Функции:

    • loadTimers() — асинхронно загружает таймеры из базы, обрабатывает ошибки и обновляет состояние загрузки;

    • toggleExpand(id) — переключает раскрытие/сворачивание таймера в списке.

  3. Хук жизненного цикла:

    • onMounted(loadTimers) — при монтировании компонента автоматически запускает загрузку таймеров.

  4. Возврат значений:

    • Все состояния и функции возвращаются из setup() для использования в шаблоне;

    • Иконки экспортируются для удобного использования в интерфейсе.

Суть: компонент отображает список таймеров из базы данных, поддерживает индикатор загрузки, обработку ошибок и возможность раскрытия деталей конкретного таймера.

index.ts

Следующее, что я реализовал в проекте — маршрутизацию. Она нужна, чтобы переходить между страницами приложения и корректно отображать компоненты Vue.

import { createRouter, createWebHistory } from '@ionic/vue-router'
import type { RouteRecordRaw } from 'vue-router'
import TimersList from '@/views/TimersList/TimersList.vue'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/timers',
  },
  {
    path: '/timers',
    name: 'Timers',
    component: TimersList,
  },
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

export default router

Разбор index.ts:

  • Импортирует функции createRouter и createWebHistory из @ionic/vue-router для настройки маршрутизатора;

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

    • / — перенаправляет на /timers;

    • /timers — отображает компонент TimersList.

  • Создаёт роутер router с историей браузера (createWebHistory) и подключёнными маршрутами;

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

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

vite-env.d.ts

Мне нужен небольшой, но важный файл, который помогает TypeScript понимать типы, связанные с Vite. Без него редактор ругался на глобальные переменные и импорты из Vite.

/// <reference types="vite/client" />

Строка /// <reference types="vite/client" /> подключает глобальные типы Vite.

TimersList.vue

Теперь я покажу, как устроен сам компонент Vue для экрана «Таймеры». В этом файле подключаются шаблон, логика на TypeScript и стили, чтобы всё было красиво и работало вместе.

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

Разбор TimersList.vue:

  • <template> — подключает HTML-шаблон компонента из отдельного файла TimersList.html;

  • <script lang="ts"> — подключает TypeScript-логику из TimersList.ts, где описан функционал компонента;

  • <style scoped> — подключает CSS-стили из TimersList.css и ограничивает их действие только этим компонентом.

Суть: этот файл объединяет шаблон, логику и стили компонента, сохраняя код чистым и модульным.

App.vue

App.vue — это корневой компонент приложения. Он задаёт базовую структуру и подключает роутер, чтобы страницы отображались корректно.

<template>
  <ion-app>
    <ion-router-outlet />
  </ion-app>
</template>

<script setup>
  import { IonApp, IonRouterOutlet } from '@ionic/vue'
</script>

Разбор App.vue:

  • <template> — оборачивает всё приложение в компонент IonApp из Ionic;

  • <ion-router-outlet /> — контейнер для отображения страниц в зависимости от маршрута;

  • <script setup> — импортирует необходимые компоненты Ionic (IonApp, IonRouterOutlet) и подключает их к шаблону.

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

main.ts

Настало время для «финального аккорда» — файла main.ts, который собирает все компоненты в приложение.

import { createApp } from 'vue'
import App from '@/App.vue'
import router from '@/router'

import { IonicVue } from '@ionic/vue'

/* Core CSS required for Ionic components to work properly */
import '@ionic/vue/css/core.css'

/* Basic CSS for apps built with Ionic */
import '@ionic/vue/css/normalize.css'
import '@ionic/vue/css/structure.css'
import '@ionic/vue/css/typography.css'

/* Optional CSS utils */
import '@ionic/vue/css/padding.css'
import '@ionic/vue/css/float-elements.css'
import '@ionic/vue/css/text-alignment.css'
import '@ionic/vue/css/text-transformation.css'
import '@ionic/vue/css/flex-utils.css'
import '@ionic/vue/css/display.css'

/* Theme variables */
import '@/theme/variables.css'

const app = createApp(App)
  .use(IonicVue)
  .use(router)

router.isReady().then(() => {
  app.mount('#app')
})

Разбор main.ts:

  • Импорт Vue и корневого компонента: createApp и App.vue;

  • Импорт роутера: подключает маршрутизацию;

  • Использование IonicVue: интегрирует компоненты и стили Ionic.

  • Подключение CSS:

    • Core CSS и базовые стили для корректной работы компонентов;

    • Опциональные утилиты для отступов, выравнивания текста, flex и display;

    • Темы и переменные проекта (variables.css).

  • Создание приложения: createApp(App).use(IonicVue).use(router).

  • Монтирование приложения: ждёт готовности роутера (router.isReady()) и монтирует в элемент с id="app".

Суть: этот файл собирает приложение из компонентов, подключает роутер и стили Ionic и запускает его в браузере.

В конце нужно обязательно поменять .js на .ts в файле index.html.

<script type="module" src="/src/main.ts"></script>

На этом написание кода для экрана «Таймеры» завершено.


Исправление проблемы c Arch используя issue

В конце прошлой статьи я рассказывал, что Сергей, который помогал мне тестировать приложение на Arch Linux, создал для меня issue в GitHub.

Issue — это запись или сообщение в репозитории, с помощью которого можно описать проблему, задачу или предложение по улучшению проекта. В нём обычно указывают:

  • что произошло (описание ошибки или задачи);

  • шаги для воспроизведения проблемы;

  • ожидаемый результат;

  • фактический результат;

  • дополнительные файлы, скриншоты или логи.

Суть: issue помогает отслеживать баги, запросы на улучшения и организовывать работу над проектом в команде.

issue
issue

Текст issue (перевод с english)

Описание ошибки: Предоставленный AppImage не запускается в Arch Linux (протестировано на Hyprland/BSPWM). В терминале отображается следующая ошибка: Не удалось создать EGL-дисплей по умолчанию: EGL_BAD_PARAMETER

Основная причина: Это известная проблема с webkit2gtk в дистрибутивах с непрерывным обновлением (например, Arch) при работе в Wayland или некоторых средах X11. Аппаратное ускорение/режим композитинга в WebKit конфликтует с инициализацией EGL-дисплея системы.

Решение (протестировано): Мне удалось успешно собрать проект из исходного кода на моей машине Arch с определенным исправлением. Добавление простого переключения переменной окружения в main.rs перед запуском сборщика Tauri полностью решает проблему.

Предложенное изменение кода: В файле desktop/src-tauri/src/main.rs измените функцию main следующим образом:

fn main() {
    // Fix for EGL_BAD_PARAMETER on Arch Linux
    #[cfg(target_os = "linux")]
    {
        std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");
    }

    println!("Starting Tauri application...");
    app_lib::run();
}

Локальная сборка: После применения этого изменения я запустил команду cargo tauri dev, и приложение запустилось без проблем.

Бинарный файл: Скомпилированный релизный бинарный файл также работает без каких-либо внешних флагов или переменных окружения.

Дополнительные замечания: Текущая структура проекта несколько сложна (разделена на папки клиента и рабочего стола). Это может вызывать проблемы для автоматических сборщиков или GitHub Actions при создании AppImage, что приводит к отсутствию ресурсов или некорректным путям. Упрощение структуры или обеспечение корректного указания файла tauri.conf.json на ресурсы фронтенда повысит надежность сборки.

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

Исправление бага

В issue написано подробно как устранить проблему, но решение слишком радикальное чтоб делать WEBKIT_DISABLE_COMPOSITING_MODE в 1 у всех на Linux.

WEBKIT_DISABLE_COMPOSITING_MODE — это переменная окружения WebKitGTK, которая принудительно отключает accelerated compositing (аппаратно-ускоренный композитинг/рендеринг) в WebKit, и её нельзя выключать всем пользователям, потому что это глобально переводит WebView на более “safe”, но потенциально более медленный/менее плавный режим и может ухудшить производительность/поведение там, где проблемы с EGL вообще нет.

Вместо радикального решения я добавил флаг --disable-webkit-compositing, которым можно отключить accelerated compositing если возникла ошибка Could not create default EGL display: EGL_BAD_PARAMETER.

И это, к сожалению, не исправило его проблему с запуском AppImage. Одна ошибка сменяла другую. Например, появилась ошибка sqfs_read_range error, которая означает, что при чтении файловой системы SquashFS внутри AppImage произошёл сбой.

sqfs_read_range error
sqfs_read_range error

Это стало для меня последней каплей. Мало того, что AppImage включает в себя все зависимости и весит 94 МБ (для сравнения, версия deb занимает всего 4,5 МБ), так ещё и в некоторых случаях чтение его файловой системы вызывает проблемы. Поэтому я решил поступить радикально и полностью от него отказаться.

Для Arch Linux есть более правильный вариант, о котором я расскажу чуть позже, а пока я уберу AppImage из проекта.

Фрагмент файла tauri.conf.json:

"targets": ["deb", "rpm"],

Я просто указал в списке "targets" форматы, которые мне нужны, а неуказанные он собирать не будет.


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

Сегодня я наконец доделал основу приложения Kawai-Focus-v2, которая позволит мне легко расширить проект до того вида, который у меня был на Kivy.

Однако у меня остаётся незакрытый «генштальт» с Arch Linux. Самое правильное для Arch, и это то, о чём меня просили изначально арчеводы, — это доб��вить описание сборки в AUR.

AUR в Arch — это Arch User Repository: пользовательский репозиторий, поддерживаемый сообществом. Он содержит не готовые бинарные пакеты, как официальные репозитории, а в основном описания сборки (PKGBUILD), по которым собирается пакет (обычно с помощью makepkg) и потом устанавливается через pacman.

В следующей статье для тестирования и более глубокого понимания операционной системы я установлю полноценный Arch Linux на свой ПК. Также попробую на нём собрать и запустить своё приложение и в конце добавить его в AUR.

Я сделал ещё кое-какие настройки для deb пакета, о которых расскажу в следующей статье, так как этот материал не поместился в текущую. Ещё хочу похвастаться, что я выложил предварительные релизы deb и rpm на GitHub. Если кто-то захочет их протестировать на своих системах, пишите о результатах в комментариях.

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

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


Заключение

  • Переписана логика с JS на TS;

  • Исправлена вторая проблема запуска на Arch по issue Сергея (пришлось отключить сборку AppImage).


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