First commit
This commit is contained in:
commit
8585f5741f
13 changed files with 505 additions and 0 deletions
41
.gitignore
vendored
Normal file
41
.gitignore
vendored
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Prerequisites
|
||||||
|
*.d
|
||||||
|
|
||||||
|
# Compiled Object files
|
||||||
|
*.slo
|
||||||
|
*.lo
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
|
||||||
|
# Precompiled Headers
|
||||||
|
*.gch
|
||||||
|
*.pch
|
||||||
|
|
||||||
|
# Compiled Dynamic libraries
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
|
||||||
|
# Fortran module files
|
||||||
|
*.mod
|
||||||
|
*.smod
|
||||||
|
|
||||||
|
# Compiled Static libraries
|
||||||
|
*.lai
|
||||||
|
*.la
|
||||||
|
*.a
|
||||||
|
*.lib
|
||||||
|
|
||||||
|
# Executables
|
||||||
|
*.exe
|
||||||
|
*.out
|
||||||
|
*.app
|
||||||
|
|
||||||
|
# clangd cache
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# CMake build directory
|
||||||
|
build
|
||||||
|
|
||||||
|
.slint.lua
|
||||||
|
.nvimrc
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "external/selenite"]
|
||||||
|
path = external/selenite
|
||||||
|
url = https://codeberg.org/vyn/selenite.git
|
35
CMakeLists.txt
Normal file
35
CMakeLists.txt
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
cmake_minimum_required(VERSION 3.21)
|
||||||
|
project(focus LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
set(SLINT_FEATURE_RENDERER_SKIA ON)
|
||||||
|
set(SLINT_FEATURE_RENDERER_SOFTWARE ON)
|
||||||
|
|
||||||
|
find_package(Slint QUIET)
|
||||||
|
if (NOT Slint_FOUND)
|
||||||
|
message("Slint could not be located in the CMake module search path. Downloading it from Git and building it locally")
|
||||||
|
include(FetchContent)
|
||||||
|
FetchContent_Declare(
|
||||||
|
Slint
|
||||||
|
GIT_REPOSITORY https://github.com/slint-ui/slint.git
|
||||||
|
# `release/1` will auto-upgrade to the latest Slint >= 1.0.0 and < 2.0.0
|
||||||
|
# `release/1.0` will auto-upgrade to the latest Slint >= 1.0.0 and < 1.1.0
|
||||||
|
GIT_TAG release/1.8
|
||||||
|
SOURCE_SUBDIR api/cpp
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(Slint)
|
||||||
|
endif (NOT Slint_FOUND)
|
||||||
|
|
||||||
|
add_executable(focus src/main.cpp)
|
||||||
|
target_link_libraries(focus PRIVATE Slint::Slint)
|
||||||
|
slint_target_sources(
|
||||||
|
focus ui/app-window.slint
|
||||||
|
NAMESPACE ui
|
||||||
|
LIBRARY_PATHS selenite=${CMAKE_CURRENT_SOURCE_DIR}/external/selenite/index.slint
|
||||||
|
)
|
||||||
|
|
||||||
|
# On Windows, copy the Slint DLL next to the application binary so that it's found.
|
||||||
|
if (WIN32)
|
||||||
|
add_custom_command(TARGET focus POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_RUNTIME_DLLS:focus> $<TARGET_FILE_DIR:focus> COMMAND_EXPAND_LISTS)
|
||||||
|
endif()
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) <year> <copyright holders>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
55
README.md
Normal file
55
README.md
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
> [!warning]
|
||||||
|
> This is a work in progress and not stable.
|
||||||
|
|
||||||
|
# Focus
|
||||||
|
|
||||||
|
Simple Pomodoro timer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Build from source
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- GCC >= 14.2.1
|
||||||
|
- CMake >= 3.30.2
|
||||||
|
- Ninja (or Make but you will need to adapt the commands)
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
Fetch and setup the repository:
|
||||||
|
```
|
||||||
|
git clone https://codeberg.org/vyn/focus.git
|
||||||
|
cd focus
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
To build:
|
||||||
|
```
|
||||||
|
cmake -DCMAKE_BUILD_TYPE=Release -S . -B ./build -G Ninja
|
||||||
|
cd build
|
||||||
|
ninja
|
||||||
|
```
|
||||||
|
Then you should have a `focus` executable in the `build` directory you are currently in.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Feel free to make suggestions and report issues, but I do **not** accept contributions (pull requests).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (C) Vyn 2024
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, under version 3 of the License only.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program in the LICENSE file.
|
||||||
|
If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
|
1
external/selenite
vendored
Submodule
1
external/selenite
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1774720377afc932cb92303516e049f66d8bf7a0
|
BIN
images/presentation-1.png
Normal file
BIN
images/presentation-1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
147
src/main.cpp
Normal file
147
src/main.cpp
Normal file
|
@ -0,0 +1,147 @@
|
||||||
|
#include "app-window.h"
|
||||||
|
#include "slint_timer.h"
|
||||||
|
#include "slint.h"
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class CountdownState {
|
||||||
|
public:
|
||||||
|
CountdownState(slint::ComponentHandle<ui::AppWindow> ui) : ui_(ui) {
|
||||||
|
ui_->global<ui::State>().set_break_countdown_duration(60 * 5);
|
||||||
|
ui_->global<ui::State>().set_focus_countdown_duration(60 * 25);
|
||||||
|
ui_->global<ui::State>().set_max_session_count(4);
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::SessionStep sessionStep() const {
|
||||||
|
return ui_->global<ui::State>().get_sessions_step();
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentSession() const {
|
||||||
|
return ui_->global<ui::State>().get_current_session();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentSession(int newCurrentSession) const {
|
||||||
|
ui_->global<ui::State>().set_current_session(newCurrentSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxSessions() const {
|
||||||
|
return ui_->global<ui::State>().get_max_session_count();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSessionStep(ui::SessionStep newSessionStep) {
|
||||||
|
ui_->global<ui::State>().set_sessions_step(newSessionStep);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui::CountdownStatus countdownStatus() const {
|
||||||
|
return ui_->global<ui::State>().get_countdown_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCountdownStatus(ui::CountdownStatus newCountdownStatus) {
|
||||||
|
ui_->global<ui::State>().set_countdown_status(newCountdownStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCountdown(int newCountdownValue) {
|
||||||
|
ui_->global<ui::State>().set_countdown(newCountdownValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
int countdown() const {
|
||||||
|
return ui_->global<ui::State>().get_countdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleCountdown() {
|
||||||
|
if (countdownStatus() == ui::CountdownStatus::Running) {
|
||||||
|
pause();
|
||||||
|
} else if (countdownStatus() == ui::CountdownStatus::Paused || countdownStatus() == ui::CountdownStatus::NotStarted) {
|
||||||
|
start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void goNextStep() {
|
||||||
|
if (sessionStep() == ui::SessionStep::Setup) {
|
||||||
|
setSessionStep(ui::SessionStep::Focus);
|
||||||
|
setCountdownStatus(ui::CountdownStatus::NotStarted);
|
||||||
|
setCountdown(ui_->global<ui::State>().get_focus_countdown_duration());
|
||||||
|
setCurrentSession(currentSession() + 1);
|
||||||
|
} else if (sessionStep() == ui::SessionStep::Focus) {
|
||||||
|
setSessionStep(ui::SessionStep::Break);
|
||||||
|
setCountdownStatus(ui::CountdownStatus::NotStarted);
|
||||||
|
setCountdown(ui_->global<ui::State>().get_break_countdown_duration());
|
||||||
|
} else if (sessionStep() == ui::SessionStep::Break) {
|
||||||
|
setCurrentSession(currentSession() + 1);
|
||||||
|
if (currentSession() < maxSessions()) {
|
||||||
|
setSessionStep(ui::SessionStep::Focus);
|
||||||
|
setCountdownStatus(ui::CountdownStatus::NotStarted);
|
||||||
|
setCountdown(ui_->global<ui::State>().get_focus_countdown_duration());
|
||||||
|
} else {
|
||||||
|
setSessionStep(ui::SessionStep::Finished);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
setCountdownStatus(ui::CountdownStatus::Running);
|
||||||
|
timer_.start(slint::TimerMode::Repeated, std::chrono::milliseconds(1000), [&]{
|
||||||
|
onTimerTick(&timer_, ui_);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void pause() {
|
||||||
|
setCountdownStatus(ui::CountdownStatus::Paused);
|
||||||
|
timer_.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
timer_.stop();
|
||||||
|
setSessionStep(ui::SessionStep::Setup);
|
||||||
|
setCountdownStatus(ui::CountdownStatus::NotStarted);
|
||||||
|
setCountdown(ui_->global<ui::State>().get_focus_countdown_duration());
|
||||||
|
setCurrentSession(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void onTimerTick(slint::Timer* timer, slint::ComponentHandle<ui::AppWindow> ui) {
|
||||||
|
int countdown = ui->global<ui::State>().get_countdown();
|
||||||
|
countdown--;
|
||||||
|
ui->global<ui::State>().set_countdown(countdown);
|
||||||
|
|
||||||
|
if (countdown == 0) {
|
||||||
|
timer->stop();
|
||||||
|
ui->global<ui::State>().set_countdown_status(ui::CountdownStatus::NotStarted);
|
||||||
|
// TODO make it customizable
|
||||||
|
system("notify-send -t 0 \"Pomodoro\" \"task\" &");
|
||||||
|
system("paplay /usr/share/sounds/freedesktop/stereo/complete.oga &");
|
||||||
|
|
||||||
|
goNextStep();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
slint::ComponentHandle<ui::AppWindow> ui_;
|
||||||
|
slint::Timer timer_;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main(int argc, char **argv)
|
||||||
|
{
|
||||||
|
slint::ComponentHandle<ui::AppWindow> ui = ui::AppWindow::create();
|
||||||
|
|
||||||
|
CountdownState countdown(ui);
|
||||||
|
|
||||||
|
ui->on_start_stop([&]{
|
||||||
|
if (countdown.sessionStep() == ui::SessionStep::Setup || countdown.sessionStep() == ui::SessionStep::Finished) {
|
||||||
|
countdown.reset();
|
||||||
|
countdown.goNextStep();
|
||||||
|
countdown.start();
|
||||||
|
} else if (countdown.sessionStep() == ui::SessionStep::Focus || countdown.sessionStep() == ui::SessionStep::Break) {
|
||||||
|
countdown.toggleCountdown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui->global<ui::State>().on_config_changed([&] {
|
||||||
|
countdown.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
ui->run();
|
||||||
|
return 0;
|
||||||
|
}
|
54
ui/app-window.slint
Normal file
54
ui/app-window.slint
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
import { State, SessionStep, CountdownStatus } from "./state.slint";
|
||||||
|
import { VText, VButton, VActionButton, Svg, Palette } from "@selenite";
|
||||||
|
import { CountdownView } from "./countdown-view.slint";
|
||||||
|
import { SettingsView } from "settings-view.slint";
|
||||||
|
|
||||||
|
enum CurrentView {
|
||||||
|
Countdown,
|
||||||
|
Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AppWindow inherits Window {
|
||||||
|
title: "Focus";
|
||||||
|
callback start-stop;
|
||||||
|
property <CurrentView> current-view: CurrentView.Countdown;
|
||||||
|
|
||||||
|
background: Palette.background;
|
||||||
|
default-font-size: 16px;
|
||||||
|
padding: 0px;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
width: parent.width;
|
||||||
|
height: parent.height;
|
||||||
|
|
||||||
|
HorizontalLayout {
|
||||||
|
padding: 8px;
|
||||||
|
vertical-stretch: 0;
|
||||||
|
alignment: start;
|
||||||
|
settingsButtons := VActionButton {
|
||||||
|
icon-svg: Svg.burger;
|
||||||
|
background: Palette.background.transparentize(1);
|
||||||
|
clicked => {
|
||||||
|
if (current-view == CurrentView.Countdown) {
|
||||||
|
current-view = CurrentView.Settings;
|
||||||
|
} else if (current-view == CurrentView.Settings) {
|
||||||
|
current-view = CurrentView.Countdown;
|
||||||
|
State.config-changed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if current-view == CurrentView.Countdown : countdown-view := CountdownView {
|
||||||
|
start-stop => { root.start-stop() }
|
||||||
|
vertical-stretch: 1;
|
||||||
|
padding: 32px;
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
if current-view == CurrentView.Settings : settings-view := SettingsView {
|
||||||
|
vertical-stretch: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { State }
|
68
ui/countdown-view.slint
Normal file
68
ui/countdown-view.slint
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import { State, SessionStep, CountdownStatus } from "./state.slint";
|
||||||
|
import { VText, VButton, VActionButton, Svg, Palette } from "@selenite";
|
||||||
|
import { Utils } from "utils.slint";
|
||||||
|
|
||||||
|
export component CountdownView inherits VerticalLayout {
|
||||||
|
callback start-stop <=> startStopButton.clicked;
|
||||||
|
|
||||||
|
pure function format-session-step(session-step: SessionStep) -> string {
|
||||||
|
if (session-step == SessionStep.Setup) {
|
||||||
|
return "Setup";
|
||||||
|
}
|
||||||
|
if (session-step == SessionStep.Focus) {
|
||||||
|
return "Focus";
|
||||||
|
}
|
||||||
|
if (session-step == SessionStep.Break) {
|
||||||
|
return "Break";
|
||||||
|
}
|
||||||
|
if (session-step == SessionStep.Finished) {
|
||||||
|
return "Finished";
|
||||||
|
}
|
||||||
|
return "Not implemented";
|
||||||
|
}
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
|
||||||
|
spacing: 32px;
|
||||||
|
alignment: center;
|
||||||
|
|
||||||
|
VerticalLayout {
|
||||||
|
VText {
|
||||||
|
font-size: 3rem;
|
||||||
|
letter-spacing: 0.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text: "\{Utils.format-countdown(State.countdown)}";
|
||||||
|
horizontal-alignment: center;
|
||||||
|
}
|
||||||
|
VText {
|
||||||
|
color: Palette.foreground-hint;
|
||||||
|
text: format-session-step(State.sessions-step);
|
||||||
|
horizontal-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: space-between;
|
||||||
|
spacing: 8px;
|
||||||
|
for step[step-index] in State.max-session-count: Rectangle {
|
||||||
|
preferred-width: 16px;
|
||||||
|
preferred-height: 16px;
|
||||||
|
border-radius: 32px;
|
||||||
|
background: step-index + 1 == State.current-session
|
||||||
|
? Palette.accent.transparentize(0.5)
|
||||||
|
: step-index + 1 < State.current-session ? Palette.accent
|
||||||
|
: Palette.control-background;
|
||||||
|
animate background {
|
||||||
|
duration: 0.5s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalLayout {
|
||||||
|
alignment: center;
|
||||||
|
startStopButton := VActionButton {
|
||||||
|
background: Palette.foreground.transparentize(1);
|
||||||
|
icon-size: 2rem;
|
||||||
|
icon-svg: State.countdown-status != CountdownStatus.Running ? Svg.play : Svg.pause;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
44
ui/settings-view.slint
Normal file
44
ui/settings-view.slint
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { State, SessionStep, CountdownStatus } from "./state.slint";
|
||||||
|
import { VText, VButton, VSlider, Palette } from "@selenite";
|
||||||
|
import { Utils } from "utils.slint";
|
||||||
|
|
||||||
|
export component SettingsView inherits Rectangle {
|
||||||
|
VerticalLayout {
|
||||||
|
padding: 32px;
|
||||||
|
spacing: 32px;
|
||||||
|
alignment: center;
|
||||||
|
VSlider {
|
||||||
|
label: "Session duration";
|
||||||
|
label-size: 1.25rem;
|
||||||
|
label-alignment: TextHorizontalAlignment.center;
|
||||||
|
no-background: true;
|
||||||
|
minimum: 1;
|
||||||
|
maximum: 60 * 60;
|
||||||
|
value: State.focus-countdown-duration;
|
||||||
|
format-value(value) => { return Utils.format-countdown(value); }
|
||||||
|
released(value) => { State.focus-countdown-duration = value; }
|
||||||
|
}
|
||||||
|
VSlider {
|
||||||
|
label: "Break duration";
|
||||||
|
label-size: 1.25rem;
|
||||||
|
label-alignment: TextHorizontalAlignment.center;
|
||||||
|
no-background: true;
|
||||||
|
minimum: 1;
|
||||||
|
maximum: 60 * 60;
|
||||||
|
value: State.break-countdown-duration;
|
||||||
|
format-value(value) => { return Utils.format-countdown(value); }
|
||||||
|
released(value) => { State.break-countdown-duration = value; }
|
||||||
|
}
|
||||||
|
VSlider {
|
||||||
|
label: "Number of session";
|
||||||
|
label-size: 1.25rem;
|
||||||
|
label-alignment: TextHorizontalAlignment.center;
|
||||||
|
no-background: true;
|
||||||
|
minimum: 1;
|
||||||
|
maximum: 8;
|
||||||
|
value: State.max-session-count;
|
||||||
|
format-value(value) => { return Math.round(value); }
|
||||||
|
released(value) => { State.max-session-count = Math.round(value); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
24
ui/state.slint
Normal file
24
ui/state.slint
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export enum SessionStep {
|
||||||
|
Setup,
|
||||||
|
Focus,
|
||||||
|
Break,
|
||||||
|
Finished,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CountdownStatus {
|
||||||
|
Running,
|
||||||
|
Paused,
|
||||||
|
NotStarted
|
||||||
|
}
|
||||||
|
|
||||||
|
export global State {
|
||||||
|
in-out property <int> countdown;
|
||||||
|
in-out property <int> focus-countdown-duration;
|
||||||
|
in-out property <int> break-countdown-duration;
|
||||||
|
in-out property <SessionStep> sessions-step;
|
||||||
|
in-out property <CountdownStatus> countdown-status;
|
||||||
|
in-out property <int> current-session;
|
||||||
|
in-out property <int> max-session-count;
|
||||||
|
|
||||||
|
callback config-changed();
|
||||||
|
}
|
12
ui/utils.slint
Normal file
12
ui/utils.slint
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export global Utils {
|
||||||
|
public pure function format-zero-padding(number: int) -> string {
|
||||||
|
if (number < 10) {
|
||||||
|
return "0\{number}";
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
public pure function format-countdown(countdown: int) -> string {
|
||||||
|
return "\{format-zero-padding(countdown / 60)}:\{format-zero-padding(Math.mod(countdown, 60))}";
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue