Compare commits
20 Commits
0.1.0-alph
...
0.2.0-alph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a943e1c08 | ||
|
|
79e87dbbe8 | ||
|
|
07b48d70a7 | ||
|
|
5f004ddcbc | ||
|
|
23239e303a | ||
|
|
196f3c6cb4 | ||
|
|
f45e23b440 | ||
|
|
35061a44f8 | ||
|
|
d56c983901 | ||
|
|
9b637a3982 | ||
|
|
5b52549404 | ||
|
|
46b8b181da | ||
|
|
a431f70c25 | ||
|
|
e7b768c926 | ||
|
|
82650e538e | ||
|
|
1716306e86 | ||
|
|
4d2bb6597a | ||
|
|
ec020b88c3 | ||
|
|
882f781ae8 | ||
|
|
8f3cadcf58 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -22,6 +22,7 @@ gen/
|
|||||||
|
|
||||||
# Database
|
# Database
|
||||||
*.db
|
*.db
|
||||||
|
*.db*
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
|
|
||||||
|
|||||||
42
README.en.md
Normal file
42
README.en.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Kawai-Focus
|
||||||
|
A focus app based on the Pomodoro technique.
|
||||||
|
|
||||||
|
The project is currently at the MVP1 development stage:
|
||||||
|
|
||||||
|
- **Pomodoro** — timers for the Pomodoro technique to manage your work sessions and rest breaks;
|
||||||
|
- **Guide** — information on how to use the timers and the app, as well as details about the Pomodoro technique itself;
|
||||||
|
- **App Limiter** — block or limit distracting applications during your work blocks.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Vue.js** — handles the user interface and SQLite operations (frontend);
|
||||||
|
- **Ionic** — provides native UI components and the mobile version;
|
||||||
|
- **Tauri** — the engine powering the desktop version of the application.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
**Project Developer:**
|
||||||
|
- **Name and Surname** — Eugene Kaddo
|
||||||
|
- **Nickname** — Arduinum628
|
||||||
|
|
||||||
|
I share detailed insights about the app's development process in my articles on [Kawai-Focus](https://pressanybutton.ru/category/kawai-focus/) at the website [Kod on napkin](https://pressanybutton.ru).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>Branch and Commit Naming Conventions</strong></summary>
|
||||||
|
|
||||||
|
Branch example: `nickname/name_task`
|
||||||
|
- **nickname** — your GitHub username;
|
||||||
|
- **name_task** — task name (matches the card title on YouGile).
|
||||||
|
|
||||||
|
Commit example: `refactor: renaming a variable`
|
||||||
|
- **feat:** — a new feature for the application code (excluding build configurations);
|
||||||
|
- **devops:** — build configuration changes (adding, removing, or fixing configurations);
|
||||||
|
- **fix:** — a bug fix in the application code;
|
||||||
|
- **docs:** — documentation changes only;
|
||||||
|
- **style:** — changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc.);
|
||||||
|
- **refactor:** — a code change that neither fixes a bug nor adds a feature (e.g., renaming a variable);
|
||||||
|
- **test:** — adding missing tests or correcting existing tests;
|
||||||
|
- **chore:** — changes to the build process or auxiliary tools and libraries such as documentation generation.
|
||||||
|
|
||||||
|
Conventions are based on [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||||
|
</details>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# kawai-focus-v2
|
# Кавай-Фокус
|
||||||
Kawai-Focus - приложение для фокусировки внимания на основе таймера Pomodoro.
|
Приложение для фокусировки внимания на основе таймера Pomodoro.
|
||||||
|
|
||||||
роект находится на стадии разработки MVP1:
|
Проект находится на стадии разработки MVP1:
|
||||||
|
|
||||||
- **Pomodoro** - таймеры для техники Pomodoro, которая управляет вашим временем работы и перерывами на отдых;
|
- **Pomodoro** - таймеры для техники Pomodoro, которая управляет вашим временем работы и перерывами на отдых;
|
||||||
- **Гайд** - информация о том как использовать таймеры и приложение, а так же о самой технике Pomodoro;
|
- **Гайд** - информация о том как использовать таймеры и приложение, а так же о самой технике Pomodoro;
|
||||||
@@ -20,7 +20,7 @@ Kawai-Focus - приложение для фокусировки внимани
|
|||||||
- **Имя и Фамилия** - Евгений Каддо
|
- **Имя и Фамилия** - Евгений Каддо
|
||||||
- **Никнейм** - Arduinum628
|
- **Никнейм** - Arduinum628
|
||||||
|
|
||||||
Подробно рассказываю о процессе написания кода приложения в своих статьях <a href="https://pressanybutton.ru/category/kawai-focus/">kawai-focus</a> на сайте <a href="https://pressanybutton.ru">Код на салфетке</a>.
|
Подробно рассказываю о процессе написания кода приложения в своих статьях <a href="https://pressanybutton.ru/category/kawai-focus/">Kawai-Focus</a> на сайте <a href="https://pressanybutton.ru">Код на салфетке</a>.
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>
|
<summary>
|
||||||
|
|||||||
26
client/package-lock.json
generated
26
client/package-lock.json
generated
@@ -16,8 +16,10 @@
|
|||||||
"@ionic/vue": "^8.0.0",
|
"@ionic/vue": "^8.0.0",
|
||||||
"@ionic/vue-router": "^8.0.0",
|
"@ionic/vue-router": "^8.0.0",
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||||
"@tauri-apps/plugin-sql": "^2.3.1",
|
"@tauri-apps/plugin-sql": "^2.3.1",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
|
"toml": "^4.1.1",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
"vue-router": "^4.2.0"
|
"vue-router": "^4.2.0"
|
||||||
},
|
},
|
||||||
@@ -3146,15 +3148,24 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@tauri-apps/api": {
|
"node_modules/@tauri-apps/api": {
|
||||||
"version": "2.9.1",
|
"version": "2.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz",
|
||||||
"integrity": "sha512-IGlhP6EivjXHepbBic618GOmiWe4URJiIeZFlB7x3czM0yDHHYviH1Xvoiv4FefdkQtn6v7TuwWCRfOGdnVUGw==",
|
"integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==",
|
||||||
"license": "Apache-2.0 OR MIT",
|
"license": "Apache-2.0 OR MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
"url": "https://opencollective.com/tauri"
|
"url": "https://opencollective.com/tauri"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-fs": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-sql": {
|
"node_modules/@tauri-apps/plugin-sql": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-sql/-/plugin-sql-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-sql/-/plugin-sql-2.3.1.tgz",
|
||||||
@@ -8971,6 +8982,15 @@
|
|||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/toml": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tough-cookie": {
|
"node_modules/tough-cookie": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
|||||||
@@ -20,8 +20,10 @@
|
|||||||
"@ionic/vue": "^8.0.0",
|
"@ionic/vue": "^8.0.0",
|
||||||
"@ionic/vue-router": "^8.0.0",
|
"@ionic/vue-router": "^8.0.0",
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"@tauri-apps/api": "^2.9.1",
|
||||||
|
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||||
"@tauri-apps/plugin-sql": "^2.3.1",
|
"@tauri-apps/plugin-sql": "^2.3.1",
|
||||||
"ionicons": "^7.0.0",
|
"ionicons": "^7.0.0",
|
||||||
|
"toml": "^4.1.1",
|
||||||
"vue": "^3.3.0",
|
"vue": "^3.3.0",
|
||||||
"vue-router": "^4.2.0"
|
"vue-router": "^4.2.0"
|
||||||
},
|
},
|
||||||
|
|||||||
4
client/public/config.toml
Normal file
4
client/public/config.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Конфиг таймера
|
||||||
|
|
||||||
|
[sound]
|
||||||
|
sound_id = "alarm_beep"
|
||||||
BIN
client/public/sounds/alarm-beep.mp3
Normal file
BIN
client/public/sounds/alarm-beep.mp3
Normal file
Binary file not shown.
35
client/src/composables/chainTimer.ts
Normal file
35
client/src/composables/chainTimer.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { TypeTimer } from '@/types/timerType';
|
||||||
|
import { getTimer } from '@/db/crud/timerCrud';
|
||||||
|
|
||||||
|
|
||||||
|
/** Фомирует цепочку таймеров */
|
||||||
|
export async function queueTimer(TimerId: number): Promise<TypeTimer[]> {
|
||||||
|
let timers: TypeTimer[] = [];
|
||||||
|
|
||||||
|
const result = await getTimer(TimerId);
|
||||||
|
const timerData = Array.isArray(result) ? result[0] : result;
|
||||||
|
|
||||||
|
for (let i = 0; i < timerData.count_pomodoro; i++) {
|
||||||
|
timers.push({
|
||||||
|
title: timerData.title,
|
||||||
|
time: timerData.pomodoro_time,
|
||||||
|
typeTimer: "Помидор"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (i === timerData.count_pomodoro - 1) {
|
||||||
|
timers.push({
|
||||||
|
title: timerData.title,
|
||||||
|
time: timerData.break_long_time,
|
||||||
|
typeTimer: "Перерывище"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
timers.push({
|
||||||
|
title: timerData.title,
|
||||||
|
time: timerData.break_time,
|
||||||
|
typeTimer: "Перерыв"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return timers;
|
||||||
|
}
|
||||||
16
client/src/composables/libAudio.ts
Normal file
16
client/src/composables/libAudio.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/** Получает путь к аудио по id */
|
||||||
|
export function getSoundPathById(id: string): string {
|
||||||
|
const sound = SOUND_LIBRARY.find(s => s.id === id);
|
||||||
|
|
||||||
|
if (!sound) {
|
||||||
|
throw new Error("Sound not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sound.file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base_path = "/sounds"
|
||||||
|
|
||||||
|
const SOUND_LIBRARY = [
|
||||||
|
{ id: "alarm_beep", file: `${base_path}/alarm-beep.mp3`, name: "Alarm Beep" }
|
||||||
|
];
|
||||||
49
client/src/composables/useAudio.ts
Normal file
49
client/src/composables/useAudio.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
let audioContext: AudioContext | null = null;
|
||||||
|
let currentSource: AudioBufferSourceNode | null = null;
|
||||||
|
|
||||||
|
/** Получить AudioContext */
|
||||||
|
function getAudioContext(): AudioContext {
|
||||||
|
if (!audioContext) {
|
||||||
|
audioContext = new AudioContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
return audioContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Воспроизводит звук */
|
||||||
|
export async function playSound(url: string): Promise<void> {
|
||||||
|
const context = getAudioContext();
|
||||||
|
|
||||||
|
if (context.state === 'suspended') {
|
||||||
|
await context.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если уже играет звук — остановит
|
||||||
|
stopSound();
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await context.decodeAudioData(arrayBuffer);
|
||||||
|
const source = context.createBufferSource();
|
||||||
|
|
||||||
|
source.buffer = audioBuffer;
|
||||||
|
source.connect(context.destination);
|
||||||
|
currentSource = source;
|
||||||
|
source.start(0);
|
||||||
|
source.onended = () => {
|
||||||
|
source.disconnect();
|
||||||
|
|
||||||
|
if (currentSource === source) {
|
||||||
|
currentSource = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Останавливает звук */
|
||||||
|
export function stopSound(): void {
|
||||||
|
if (currentSource) {
|
||||||
|
currentSource.stop();
|
||||||
|
currentSource.disconnect();
|
||||||
|
currentSource = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
client/src/composables/useConfig.ts
Normal file
36
client/src/composables/useConfig.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
|
||||||
|
import TOML from 'toml';
|
||||||
|
import { Config } from '@/types/configType';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузит или создаст TOML-конфиг
|
||||||
|
*/
|
||||||
|
export async function loadConfig(): Promise<Config> {
|
||||||
|
let text: string | null = null;
|
||||||
|
const configName = 'config.toml';
|
||||||
|
|
||||||
|
// 1. пробуем прочитать пользовательский конфиг
|
||||||
|
try {
|
||||||
|
text = await readTextFile(configName, {
|
||||||
|
baseDir: BaseDirectory.AppConfig
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
text = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. если нет — создаст из дефолта
|
||||||
|
if (!text) {
|
||||||
|
const defaultConfig = await fetch(`/${configName}`)
|
||||||
|
.then(r => r.text());
|
||||||
|
|
||||||
|
await writeTextFile(configName, defaultConfig, {
|
||||||
|
baseDir: BaseDirectory.AppConfig
|
||||||
|
});
|
||||||
|
|
||||||
|
text = defaultConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. парсим TOML
|
||||||
|
return TOML.parse(text) as Config;
|
||||||
|
}
|
||||||
120
client/src/composables/useTimer.ts
Normal file
120
client/src/composables/useTimer.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import { CountdownOptions, CountdownReturn } from '@/types/timerType';
|
||||||
|
import { stopSound } from '@/composables/useAudio';
|
||||||
|
import { TypeTimer } from '@/types/timerType';
|
||||||
|
|
||||||
|
|
||||||
|
/** Composable для управления обратным отсчётом времени */
|
||||||
|
export function useCountdown({ timers, onFinish }: CountdownOptions): CountdownReturn {
|
||||||
|
const totalSeconds = ref<number>(0);
|
||||||
|
const timerNow = ref<TypeTimer | undefined>(timers.shift());
|
||||||
|
|
||||||
|
if (timerNow.value) {
|
||||||
|
totalSeconds.value = timerNow.value.time * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Реактивное оставшееся время в секундах */
|
||||||
|
const timeLeft = ref<number>(totalSeconds.value);
|
||||||
|
|
||||||
|
/** Флаг активного состояния таймера */
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (timerNow.value) {
|
||||||
|
return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Завершён!";
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Вычисляет процент прошедшего времени от общего */
|
||||||
|
const progress = computed<number>(() => {
|
||||||
|
if (totalSeconds.value === 0) return 0;
|
||||||
|
|
||||||
|
return ((totalSeconds.value - timeLeft.value) / totalSeconds.value) * 100;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Отслеживает завершённость таймера */
|
||||||
|
const isFinished = computed<boolean>(() => {
|
||||||
|
return !timerNow.value || timeLeft.value <= 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Задаёт следующий таймер */
|
||||||
|
const setNextTimer = (): void => {
|
||||||
|
timerNow.value = timers.shift();
|
||||||
|
|
||||||
|
if (timerNow.value) {
|
||||||
|
totalSeconds.value = timerNow.value.time * 60;
|
||||||
|
timeLeft.value = totalSeconds.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
//** Заканчивает таймер */
|
||||||
|
const finish = (): void => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning.value = false;
|
||||||
|
timeLeft.value = 0;
|
||||||
|
|
||||||
|
onFinish?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Запускает таймер */
|
||||||
|
const start = (): void => {
|
||||||
|
if (isRunning.value) return
|
||||||
|
isRunning.value = true;
|
||||||
|
|
||||||
|
interval = setInterval(() => {
|
||||||
|
if (timeLeft.value > 0) {
|
||||||
|
timeLeft.value--;
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Останавливает таймер и сбрасывает время до следующего таймера */
|
||||||
|
const stop = (): void => {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRunning.value = false;
|
||||||
|
|
||||||
|
stopSound();
|
||||||
|
setNextTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Приостанавливает таймер без сброса оставшегося времени */
|
||||||
|
const pause = (): void => {
|
||||||
|
if (!isRunning.value) return
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
interval = null;
|
||||||
|
}
|
||||||
|
isRunning.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timeLeft,
|
||||||
|
isRunning,
|
||||||
|
isFinished,
|
||||||
|
formatted,
|
||||||
|
progress,
|
||||||
|
timerNow,
|
||||||
|
start,
|
||||||
|
stop,
|
||||||
|
pause
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getDb } from "@/db/initDb";
|
import { getDb } from "@/db/initDb";
|
||||||
import { SELECT_TIMERS } from "@/db/dml/timerDML";
|
import { SELECT_TIMERS, SELECT_TIMER } from "@/db/dml/timerDML";
|
||||||
import { TimersRow } from "@/types/timerType"
|
import { TimersRow, TimerRow } from "@/types/timerType";
|
||||||
|
|
||||||
|
|
||||||
/** Получает список таймеров */
|
/** Получает список таймеров */
|
||||||
@@ -8,3 +8,9 @@ export async function getTimers(): Promise<TimersRow[]> {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
return await db.select<TimersRow[]>(SELECT_TIMERS);
|
return await db.select<TimersRow[]>(SELECT_TIMERS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Получает таймер по его id **/
|
||||||
|
export async function getTimer(TimerId: number): Promise<TimerRow> {
|
||||||
|
const db = await getDb();
|
||||||
|
return await db.select<TimerRow>(SELECT_TIMER, [TimerId]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ export const INSERT_SEED_DB = `
|
|||||||
INSERT INTO timer (title, pomodoro_time, break_time, break_long_time, count_pomodoro) VALUES
|
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)
|
('Timer mini example', 10, 3, 15, 2), ('Timer max example', 90, 10, 40, 8)
|
||||||
`
|
`
|
||||||
|
export const SELECT_TIMER = 'SELECT * FROM timer WHERE id = ?'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from '@ionic/vue-router';
|
import { createRouter, createWebHistory } from '@ionic/vue-router';
|
||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
import TimersList from '@/views/TimersList/TimersList.vue';
|
import TimersList from '@/views/TimersList/TimersList.vue';
|
||||||
|
import Timer from '@/views/Timer/Timer.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@@ -11,6 +12,11 @@ const routes: RouteRecordRaw[] = [
|
|||||||
path: '/timers',
|
path: '/timers',
|
||||||
name: 'Timers',
|
name: 'Timers',
|
||||||
component: TimersList
|
component: TimersList
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/timer/:id',
|
||||||
|
name: 'Timer',
|
||||||
|
component: Timer
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
6
client/src/types/configType.ts
Normal file
6
client/src/types/configType.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
//** Тип для конфига */
|
||||||
|
export type Config = {
|
||||||
|
sound: {
|
||||||
|
sound_id: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import type { Ref, ComputedRef } from 'vue';
|
||||||
|
|
||||||
|
/** Строка списка таймеров с основными полями */
|
||||||
export type TimersRow = {
|
export type TimersRow = {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -5,4 +8,41 @@ export type TimersRow = {
|
|||||||
count_pomodoro: number;
|
count_pomodoro: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Тип для хранения результата COUNT-запроса */
|
||||||
export type CountRow = { cnt: number };
|
export type CountRow = { cnt: number };
|
||||||
|
|
||||||
|
/** Полная строка таймера со всеми временными параметрами */
|
||||||
|
export type TimerRow = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
pomodoro_time: number;
|
||||||
|
break_time: number;
|
||||||
|
break_long_time: number;
|
||||||
|
count_pomodoro: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Параметры инициализации обратного отсчёта */
|
||||||
|
export type CountdownOptions = {
|
||||||
|
timers: TypeTimer[];
|
||||||
|
onFinish?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Возвращаемые значения и методы composable обратного отсчёта */
|
||||||
|
export type CountdownReturn = {
|
||||||
|
timeLeft: Ref<number>;
|
||||||
|
isRunning: Ref<boolean>;
|
||||||
|
formatted: ComputedRef<string>; // ЧЧ:ММ:СС
|
||||||
|
progress: Ref<number>;
|
||||||
|
start: () => void;
|
||||||
|
stop: () => void;
|
||||||
|
pause: () => void;
|
||||||
|
timerNow: Ref<TypeTimer | undefined>;
|
||||||
|
isFinished: ComputedRef<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Тип таймеров */
|
||||||
|
export type TypeTimer = {
|
||||||
|
title: string;
|
||||||
|
time: number;
|
||||||
|
typeTimer: string;
|
||||||
|
}
|
||||||
|
|||||||
173
client/src/views/Timer/Timer.css
Normal file
173
client/src/views/Timer/Timer.css
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
* {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
70
client/src/views/Timer/Timer.html
Normal file
70
client/src/views/Timer/Timer.html
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
<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 class="label-left">{{ countdown?.timerNow.value?.title }}</span>
|
||||||
|
<span class="label-right">{{ countdown?.timerNow.value?.typeTimer }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time display -->
|
||||||
|
<div class="timer-time">
|
||||||
|
<span class="time-text">{{ countdown?.formatted }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div v-show="!countdown?.isFinished.value" 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' && !countdown?.isFinished.value || 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' && !countdown?.isFinished.value"
|
||||||
|
@click="() => { setState('paused'); countdown?.pause(); }"
|
||||||
|
>
|
||||||
|
<ion-icon :icon="pauseOutline"></ion-icon>
|
||||||
|
Пауза
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
87
client/src/views/Timer/Timer.ts
Normal file
87
client/src/views/Timer/Timer.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
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 type { TimerRow } from '@/types/timerType';
|
||||||
|
import { useCountdown } from '@/composables/useTimer';
|
||||||
|
import { playSound } from '@/composables/useAudio';
|
||||||
|
import { getSoundPathById } from '@/composables/libAudio';
|
||||||
|
import { loadConfig } from '@/composables/useConfig';
|
||||||
|
import { queueTimer } from '@/composables/chainTimer';
|
||||||
|
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);
|
||||||
|
|
||||||
|
/** Чтение конфига */
|
||||||
|
const config = loadConfig()
|
||||||
|
|
||||||
|
/** Останавливает таймер при размонтировании компонента */
|
||||||
|
onUnmounted(() => {
|
||||||
|
countdown.value?.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Загружает данные таймера из БД и инициализирует отсчёт */
|
||||||
|
const loadTimer = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const timers = await queueTimer(id);
|
||||||
|
|
||||||
|
countdown.value = useCountdown({
|
||||||
|
timers: timers,
|
||||||
|
onFinish: async () => {
|
||||||
|
await playSound(getSoundPathById((await config).sound.sound_id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error.value = 'Ошибка загрузки таймера';
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(loadTimer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
timer,
|
||||||
|
error,
|
||||||
|
state,
|
||||||
|
setState,
|
||||||
|
stopOutline,
|
||||||
|
playOutline,
|
||||||
|
pauseOutline,
|
||||||
|
countdown
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
4
client/src/views/Timer/Timer.vue
Normal file
4
client/src/views/Timer/Timer.vue
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<!-- 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>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<ion-page>
|
||||||
|
<ion-content>
|
||||||
<div class="timers-container">
|
<div class="timers-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>Таймеры</h1>
|
<h1>Таймеры</h1>
|
||||||
@@ -61,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="btn btn-primary">
|
<button @click="goToTimer(timer.id)" class="btn btn-secondary">
|
||||||
<ion-icon :icon="playCircleOutline"></ion-icon>
|
<ion-icon :icon="playCircleOutline"></ion-icon>
|
||||||
Открыть
|
Открыть
|
||||||
</button>
|
</button>
|
||||||
@@ -79,3 +81,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineComponent, onMounted, ref } from 'vue'
|
import { defineComponent, onMounted, ref } from 'vue'
|
||||||
import { IonIcon } from '@ionic/vue'
|
import { IonIcon, IonPage, IonContent } from '@ionic/vue'
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
import {
|
import {
|
||||||
timerOutline,
|
timerOutline,
|
||||||
chevronUp,
|
chevronUp,
|
||||||
@@ -14,13 +15,19 @@ import type { TimersRow } from '@/types/timerType'
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'TimersList',
|
name: 'TimersList',
|
||||||
components: { IonIcon },
|
components: { IonIcon, IonPage, IonContent },
|
||||||
setup() {
|
setup() {
|
||||||
const timers = ref<TimersRow[]>([])
|
const timers = ref<TimersRow[]>([])
|
||||||
const loading = ref<boolean>(true)
|
const loading = ref<boolean>(true)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const expandedId = ref<number | null>(null)
|
const expandedId = ref<number | null>(null)
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goToTimer = (id: number) => {
|
||||||
|
router.push(`/timer/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
const loadTimers = async (): Promise<void> => {
|
const loadTimers = async (): Promise<void> => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@@ -55,6 +62,7 @@ export default defineComponent({
|
|||||||
playCircleOutline,
|
playCircleOutline,
|
||||||
pencilOutline,
|
pencilOutline,
|
||||||
trashOutline,
|
trashOutline,
|
||||||
|
goToTimer
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
1860
desktop/src-tauri/Cargo.lock
generated
1860
desktop/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "kawai-focus"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "Kawai-Focus - приложение для фокусировки внимания на основе таймера Pomodoro."
|
description = "Кавай-Фокус (Kawai-Focus) - приложение для фокусировки внимания на основе таймера Pomodoro."
|
||||||
authors = ["Arduinum"]
|
authors = ["Arduinum"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = ""
|
repository = ""
|
||||||
@@ -11,7 +11,7 @@ rust-version = "1.77.2"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "app_lib"
|
name = "kawai_focus"
|
||||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@@ -24,3 +24,4 @@ log = "0.4"
|
|||||||
tauri = { version = "2.9.5", features = [] }
|
tauri = { version = "2.9.5", features = [] }
|
||||||
tauri-plugin-log = "2"
|
tauri-plugin-log = "2"
|
||||||
tauri-plugin-sql = { version = "2.3.1", features = ["sqlite"] }
|
tauri-plugin-sql = { version = "2.3.1", features = ["sqlite"] }
|
||||||
|
tauri-plugin-fs = "2.5.1"
|
||||||
|
|||||||
@@ -5,6 +5,20 @@
|
|||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default",
|
"core:default",
|
||||||
"sql:default",
|
"sql:default",
|
||||||
"sql:allow-execute"
|
"sql:allow-execute",
|
||||||
|
"fs:allow-appconfig-read",
|
||||||
|
"fs:allow-appconfig-write"
|
||||||
|
],
|
||||||
|
"fs": {
|
||||||
|
"read": {
|
||||||
|
"scope": [
|
||||||
|
"$APPCONFIG/**"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"scope": [
|
||||||
|
"$APPCONFIG/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
44
desktop/src-tauri/src/flags.rs
Normal file
44
desktop/src-tauri/src/flags.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn apply_linux_cli_flags() {
|
||||||
|
let disable_webkit_compositing_mode = std::env::args().any(|a| a == "--webkit-disable-compositing-mode");
|
||||||
|
if disable_webkit_compositing_mode {
|
||||||
|
unsafe { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
let webkit_disable_dmabuf_renderer = std::env::args().any(|a| a == "--webkit-disable-dmabuf-renderer");
|
||||||
|
if webkit_disable_dmabuf_renderer {
|
||||||
|
unsafe { std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
let nv_disable_explicit_sync = std::env::args().any(|a| a == "--nv-disable-explicit-sync");
|
||||||
|
if nv_disable_explicit_sync {
|
||||||
|
unsafe { std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1"); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn print_help() {
|
||||||
|
println!(
|
||||||
|
r#"Kawai Focus — Pomodoro таймер
|
||||||
|
|
||||||
|
Использование:
|
||||||
|
kawai-focus [FLAGS]
|
||||||
|
|
||||||
|
Флаги (Linux):
|
||||||
|
|
||||||
|
--webkit-disable-compositing-mode
|
||||||
|
Отключает WebKit compositing mode.
|
||||||
|
Помогает при графических артефактах на некоторых GPU.
|
||||||
|
|
||||||
|
--webkit-disable-dmabuf-renderer
|
||||||
|
Отключает DMA-BUF renderer WebKit.
|
||||||
|
Полезно при проблемах с Wayland и с отсутствием GBM.
|
||||||
|
|
||||||
|
--nv-disable-explicit-sync
|
||||||
|
Отключает explicit sync драйвера NVIDIA.
|
||||||
|
Иногда исправляет: зависания интерфейса, проблемы с отсутствием GBM.
|
||||||
|
|
||||||
|
--help
|
||||||
|
Показать эту справку и выйти.
|
||||||
|
"#);
|
||||||
|
}
|
||||||
@@ -1,17 +1,19 @@
|
|||||||
#[cfg(target_os = "linux")]
|
mod flags;
|
||||||
fn apply_linux_webkit_cli_flags() {
|
|
||||||
let disable = std::env::args().any(|a| a == "--disable-webkit-compositing");
|
|
||||||
if disable {
|
|
||||||
unsafe { std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1"); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
apply_linux_webkit_cli_flags();
|
#[cfg(target_os = "linux")]
|
||||||
|
if std::env::args().any(|a| a == "--help" || a == "-h") {
|
||||||
|
flags::print_help();
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
flags::apply_linux_cli_flags();
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_sql::Builder::default().build())
|
.plugin(tauri_plugin_sql::Builder::default().build())
|
||||||
|
.plugin(tauri_plugin_fs::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("Ошибка при запуске приложения Tauri.");
|
.expect("Ошибка при запуске приложения Tauri.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("Запуск приложения Tauri...");
|
println!("Запуск приложения Tauri...");
|
||||||
app_lib::run();
|
kawai_focus::run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Kawai-Focus",
|
"productName": "Kawai-Focus",
|
||||||
"version": "0.1.0",
|
"version": "0.2.0",
|
||||||
"identifier": "io.github.arduinum.KawaiFocus",
|
"identifier": "io.github.arduinum.KawaiFocus",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../../client/dist",
|
"frontendDist": "../../client/dist",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"label": "main",
|
"label": "main",
|
||||||
"title": "Kawai-Focus",
|
"title": "Кавай-Фокус",
|
||||||
"width": 800,
|
"width": 800,
|
||||||
"height": 600,
|
"height": 600,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
@@ -37,7 +37,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shortDescription": "Kawai-Focus - приложение для фокусировки внимания на основе таймера Pomodoro.",
|
"shortDescription": "Кавай-Фокус (Kawai-Focus) - приложение для фокусировки внимания на основе таймера Pomodoro.",
|
||||||
|
"copyright": "© 2026 Eugene Kaddo",
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
Reference in New Issue
Block a user