pax_global_header00006660000000000000000000000064152062567650014530gustar00rootroot0000000000000052 comment=921e587806a47fb0d6bb071ffe4e885df5aed96a focustimerhq-FocusTimer-8581be2/000077500000000000000000000000001520625676500166575ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/.editorconfig000066400000000000000000000010341520625676500213320ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf trim_trailing_whitespace = true [*.vala] indent_size = 4 tab_size = 4 indent_style = space max_line_length = 100 [*.css] indent_size = 4 tab_size = 4 indent_style = space [*.json] indent_size = 2 tab_size = 2 indent_style = space [*.ui] indent_size = 2 tab_size = 2 indent_style = space [*.{xml.in,xml}] indent_size = 2 tab_size = 2 indent_style = space [meson.build] indent_size = 2 indent_style = space [NEWS] indent_size = 2 tab_size = 2 indent_style = space max_line_length = 72 focustimerhq-FocusTimer-8581be2/.github/000077500000000000000000000000001520625676500202175ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001520625676500224025ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/.github/ISSUE_TEMPLATE/BUG_REPORT.md000066400000000000000000000007541520625676500244420ustar00rootroot00000000000000--- name: "🐞 Bug Report" about: Create a report to help us improve title: '' labels: 'type:Bug' assignees: '' --- ## Issue Summary Describe your issue here. ## Steps to Reproduce 1. ... 2. ... 3. ... ## Context (logs, error output, screencast, OS, Desktop etc) (Write your answer here.) ## Expected Behavior (Write your answer here.) ## Current Behavior (Write your answer here.) ## Possible Solution (Write your answer here.) ## Other information (Write your answer here.) focustimerhq-FocusTimer-8581be2/.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md000066400000000000000000000006311520625676500252270ustar00rootroot00000000000000--- name: "🚀 Feature request" about: Suggest an idea for improving gnome-pomodoro title: '' labels: 'type:Enhancement' assignees: '' --- ## Is your proposal related to a problem? (Write your answer here.) ## Describe the solution you'd like (Describe your proposed solution here.) ## Describe alternatives you've considered (Write your answer here.) ## Other information (Write your answer here.) focustimerhq-FocusTimer-8581be2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000002711520625676500243720ustar00rootroot00000000000000blank_issues_enabled: false contact_links: - name: GNOME Pomodoro Community Support url: https://gnomepomodoro.org/#disqus_thread about: Please ask and answer questions here. focustimerhq-FocusTimer-8581be2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000011301520625676500240130ustar00rootroot00000000000000## Pull request checklist - [ ] Lint and unit tests pass locally with my changes - [ ] Extended the README / documentation, if necessary ## Pull request type Please check the type of change your PR introduces: - [ ] Bugfix - [ ] Feature - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes - [ ] Documentation content changes - [ ] Other (please describe): ## What is the current behavior? Issue Number: N/A ## What is the new behavior? (Write your answer here.) ## Other information (Write your answer here.) focustimerhq-FocusTimer-8581be2/CONTRIBUTING.md000066400000000000000000000076001520625676500211130ustar00rootroot00000000000000# Contributing to Focus Timer Thanks for considering contributing! Whether you're fixing bugs, translating, or adding features, your help is incredibly valuable. ## Reporting issues Found a bug? Please check the [Troubleshooting](#troubleshooting) section first. When opening an issue on [GitHub](https://github.com/focustimerhq/FocusTimer/issues), include: - App version - Desktop environment and version - Relevant [logs](#getting-logs) or a [stack trace](#getting-a-stack-trace) - Steps to reproduce the bug Suggestions and feature requests are always welcome! ## Troubleshooting ### Missing indicator If you're on GNOME, you'll need the [Focus Timer extension](https://github.com/focustimerhq/gnome-shell-extension-focus-timer). Check its [troubleshooting page](https://github.com/focustimerhq/gnome-shell-extension-focus-timer/blob/main/CONTRIBUTING.md#troubleshooting) if you already have it. ### Missing Automation panel If you installed the app via Flathub, restricted permissions hide this panel. Try another [installation option](README.md#installation) if you need it. ### Getting logs Since boot: ```bash journalctl --user -b -t io.github.focustimerhq.FocusTimer ``` Real-time: ```bash journalctl --user -f -t io.github.focustimerhq.FocusTimer ``` With debug output. Run in the terminal and reproduce the issue: ```bash flatpak run io.github.focustimerhq.FocusTimer --quit flatpak run --env=G_MESSAGES_DEBUG=focus-timer io.github.focustimerhq.FocusTimer ``` ### Getting a stack trace If the app crashes, providing steps to reproduce it is usually enough. However, to share a stack trace, install the debug info: ```bash flatpak install --user --include-sdk --include-debug io.github.focustimerhq.FocusTimer ``` Find the app PID from recent crashes: ```bash coredumpctl list --since=today focus-timer ``` Run gdb with your ``: ```bash flatpak-coredumpctl -m io.github.focustimerhq.FocusTimer ``` Inside gdb, get the full trace: ```gdb bt full ``` ## Translating We use Gettext, with `.pot` and `.po` files located in the [`po/` directory](po). Note that the app and the [GNOME Shell extension](https://github.com/focustimerhq/gnome-shell-extension-focus-timer) have separate translations. **To add or update a language:** 1. Generate or update your language's `.po` file in `po/` (e.g., using `msginit`). 2. Add your language to the [`po/LINGUAS`](po/LINGUAS) file. 3. Submit a Pull Request. LLM prompt to ease the process: > Fill-in missing translations and update fuzzy translations for the given file. Translations are for an desktop Pomodoro timer app. Take care to use consistent same translations for words: "break", "pause", "start", "stop", "rewind", "interruption". "pause" refers to the timer action, while "break" refers to taking a break from work, "interruption" refers to a distraction. Translations does not need to be exact, but must convey same meaning - make them sound natural. Mark modified entries as fuzzy. Output the updated .po file for download, do not truncate it. *Note: We keep `.po` files synced with the `.pot` template automatically, so you don't need to do this manually.* ## Development We recommend using [GNOME Builder](https://flathub.org/en/apps/org.gnome.Builder) with the provided Flatpak manifest. When running `Devel` manifest some features will not work, including notifications and background indicator. SQLite database will be separate from user session. ### Running unit tests Run: ```bash ninja -C build test ``` or run unit tests through your IDE. ### Useful Resources * [Vala Documentation](https://docs.vala.dev/tutorials/programming-language/main.html) * [Vala Bindings](https://valadoc.org/index.htm) * [Adwaita Documentation](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/index.html) * [Meson Reference Manual](https://mesonbuild.com/Reference-manual.html) * [Portals Documentation](https://flatpak.github.io/xdg-desktop-portal/docs/api-reference.html) focustimerhq-FocusTimer-8581be2/COPYING000066400000000000000000001045171520625676500177220ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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, either version 3 of the License, or (at your option) any later version. 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. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . focustimerhq-FocusTimer-8581be2/NEWS000066400000000000000000000302311520625676500173550ustar00rootroot000000000000002026-05-29 Kamil Prusko Release version 1.1.2: * Deeper integration with notification servers * Idle detection on Wayland * Improve screensaver detection * Fix autostart 2026-05-07 Kamil Prusko Release version 1.1.1: * System tray icon * Smoother sound transitions * Fix break overlay scaling on HiDPI displays * Fix missing sounds after switching soundcards 2026-04-16 Kamil Prusko Release version 1.1.0: * Support for GNOME Shell extension * Option to autostart on login * Reviewed sound files * Fix build with vala 0.56.19 2026-03-01 Kamil Prusko Release version 1.0: * Fix break overlay scaling on HiDPI displays (thanks @scholzri) * Automatic daily backup * Removed libcanberra backend for playing notification sounds * Updated Lithuanian translation (thanks @psukys) * Updated Russian translation (thanks @ViktorOn) 2026-02-11 Kamil Prusko Release version 0.29.0: * Support for GNOME Shell 50 * Migrated to libpeas-2 2026-01-20 Kamil Prusko Release version 1.0-beta: * Ported the app to GTK+4 and libadwaita * Updated Swedish translation (thanks @haaninjo) * Updated Georgian translation (thanks @NorwayFun) * Updated Indonesian translation (thanks @atriwidada) 2025-12-18 Kamil Prusko Release version 0.28.1: * Added Tamil translation (thanks @omeritzics) * Added Hebrew translation (thanks @Killersparrow1) 2025-09-18 Kamil Prusko Release version 0.28.0: * Support for GNOME Shell 49 (thanks @aleasto) * Updated German translation (thanks @daPhipz) 2025-02-22 Kamil Prusko Release version 0.27.0: * Support for GNOME Shell 48 * Split time spent across midnight * Added Telugu translation (thanks @SpaciousCoder78) 2024-09-10 Kamil Prusko Release version 0.26.0: * Support for GNOME Shell 47 * Allow to dismiss screen overlay by gesture when a video is playing * Added Georgian translation (thanks @NorwayFun) * Adjusted translations in appdata (thanks @yakushabb) 2024-05-31 Kamil Prusko Release version 0.25.2: * Fix keeping notification after extending Pomodoro 2024-03-28 Kamil Prusko Release version 0.25.1: * Fixes for GNOME Shell 46 * Drop support for GNOME Shell 45 2024-03-05 Kamil Prusko Release version 0.25.0: * Support for GNOME Shell 46 * Adjust build script to meson 0.59.0 (thanks @mattst88) 2023-12-04 Kamil Prusko Release version 0.24.1: * Fixed timerState tracking and waking up of screen on the screen shield (thanks @real-or-random) 2023-08-27 Kamil Prusko Release version 0.24.0: * Marked extension as compatible with GNOME Shell 45 with no backwards compatibility * Fixed alignment of the indicator menu * Changed the default view in the app to Timer 2023-03-31 Kamil Prusko Release version 0.23.1: * Fixed invalid appdata file 2023-03-26 Kamil Prusko Release version 0.23.0: * Marked extension as compatible with GNOME Shell 44 (thanks @jbicha) * Added skip button * Lock-screen widget * Fixed annoucement notification getting dismissed 2023-02-05 Kamil Prusko Release version 0.22.1: * Close screen overlay by hitting Esc key - a failsafe method * Don't open screen overlay while a video call app is fullscreen * Fixed screen overlays timer label getting ellipsized * Fixed screen overlay being over screensaver animation * Add --reset commandline option 2022-10-02 Kamil Prusko Release version 0.22.0: * Marked extension as compatible with GNOME Shell 43 (thanks @jbicha) * Fixed blur effect in GNOME Shell 42 and later * Fixed GNOME detection on Ubuntu (thanks @real-or-random) * Fixed break overlay geting stuck at 0:01 on GNOME 3.38, 40 and 41 (thanks @prahal) * Improved indicator rendering on HiDPI screens * Updated Brazilian translation (thanks @costaronaldo) * Updated Russian translation (thanks @ViktorOn) * Updated Chinese translation (thanks @HaorongX) 2022-04-01 Kamil Prusko Release version 0.21.1: * Fixed break overlay geting stuck at 0:01 on GNOME 42 (thanks @upsuper) 2022-04-01 Kamil Prusko Release version 0.21.0: * Support for GNOME Shell 42 (thanks @milotype and @kappa) * Fixed app hanging at startup * Added Croatian translation (thanks @dayeondev) * Added json-glib and cairo as dependencies 2021-09-25 Kamil Prusko Release version 0.20.0: * Support for GNOME Shell 41 (thanks @mbooth101) 2021-09-19 Kamil Prusko Release version 0.19.2: * Fixed GNOME Shell freezing the lockscreen * Fixed unresponsive modal dialog * Updated Russian translation (thanks @prokoudine) * Updated Dutch translation (thanks @Vistaus) 2021-04-09 Kamil Prusko Release version 0.19.1: * Support GNOME Shell 40.0, not 4.0 2021-04-04 Kamil Prusko Release version 0.19.0: * Support for GNOME Shell 4.0 * Added support for meson (thanks @Cogitri) * Changed blur effect during break * Added Korean translation (thanks @dayeondev) * Updated Brazilian translation (thanks @alexandreafa) 2020-10-04 Kamil Prusko Release version 0.18.0: * Support for GNOME Shell 3.38 (thanks @ignapk and @szpak) * Removed ayatana-appindicator3 support * Added Norwegian translation (thanks @arnotixe) * Added Finnish translation (thanks @iqqmuT) * Updated Indonesian translation (thanks @atriwidada) * Updated Chinese translation (thanks @wffger) * Updated Russian translation (thanks @rkaverin) * Updated French translation (thanks @precondition) 2020-03-22 Kamil Prusko Release version 0.17.0: * Support for GNOME Shell 3.36 * Updated Catalan translation (thanks @antoniofsm) 2019-10-01 Kamil Prusko Release version 0.16.0: * Support for GNOME Shell 3.34 * Added esperanto translation (thanks @SeZuo) * Moved app-menu to main window 2019-04-07 Arun Mahapatra Release version 0.15.1: * Minor code cleanups 2019-04-07 Arun Mahapatra Release version 0.15.0: * Minor code cleanups to support ES6 syntax * Support for GNOME Shell 3.32 (thanks @demokritos) * Fix for build with vala 0.44.1 (thanks @snizovtsev) * Updated German translation (thanks @c7hm4r) * Fix for handle error recreating existing folder (thanks @Rj7) 2018-11-24 Kamil Prusko Release version 0.14.0: * Support for GNOME Shell 3.28 and 3.30 (@thanks aerostitch) * Stats * Background blur under the dialog during breaks * Updated German translation (thanks @tsabsch) * Updated Russian translation (thanks @tigertv) 2017-11-20 Kamil Prusko Release version 0.13.4: * Fix for causing gnome-shell segfault (thanks @berenddeschouwer and @3v1n0) 2017-09-06 Kamil Prusko Release version 0.13.3: * Support for GNOME Shell 3.26 * Added Dutch translation (thanks @qtk) * Added Swedish translation (thanks @haaninjo) 2017-05-07 Kamil Prusko Release version 0.13.2: * Added Italian translation (thanks @Fastbyte01) * Added Indonesian translation (thanks @aliterm) * Added Kazakh translation (thanks @crayxt) * Updated Chinese translation (thanks @soiamsoNG) * Fixed hiding system notifications in GNOME * Fixed changing notification sounds 2017-02-20 Kamil Prusko Release version 0.13.1: * Support for GNOME Shell 3.24 * Bug fixes 2017-01-02 Kamil Prusko Release version 0.13.0: * Plugin for custom actions * Removed libgnome-desktop-3.0 dependency * Updated translations * Bug fixes 2016-09-19 Kamil Prusko Release version 0.12.3: * Support for GNOME Shell 3.22 2016-08-07 Kamil Prusko Release version 0.12.2: * Plugin for appindicator * Fixed issues with fallback mode disabling 2016-06-17 Kamil Prusko Release version 0.12.1: * Fix for build error 2016-06-11 Kamil Prusko Release version 0.12.0: * Support for more desktops * Ability to pause the timer * Ability add plugins * Code refactoring * Bug fixes * Dropped Telepathy and Skype integration * Dropped support for GNOME Shell 3.14 and older 2016-05-06 Kamil Prusko Release version 0.11.3: * Ignore small mouse movements in screen notification * Fixed indicator not updating * Cleaned up some compilation warnings (thanks @aerostitch) 2016-03-24 Kamil Prusko Release version 0.11.2: * Support for GNOME Shell 3.20 2015-10-24 Kamil Prusko Release version 0.11.1: * Support for GNOME Shell 3.18 * Turkish translation (thanks @ErkanMDR) 2015-06-13 Kamil Prusko Release version 0.11.0: * Support for GNOME Shell 3.14 and 3.16 * Ability to change indicator appearance * Skype and Epiphany integration * Updated French translation (thanks @neitsab) * Code refactoring * Basic unit tests * Bug fixes 2014-10-17 Kamil Prusko Release version 0.10.3: * Support for GNOME Shell 3.10, 3.12 and 3.14 * Updated Catalan translation (thanks @Ecron) * Code cleanups (thanks @aerostitch) * Bug fixes 2014-06-13 Kamil Prusko Release version 0.10.2: * No need to restart gnome-shell to enable extension 2014-05-02 Kamil Prusko Release version 0.10.1: * Support for GNOME Shell 3.10 and 3.12 * Louder sounds * Fixed brining preferences dialog to focus * Fixed change of notications volume * Fixed reminders being showed up during pomodoro 2014-01-11 Kamil Prusko Release version 0.10.0: * Support for GNOME Shell 3.10 * New layout in preferences dialog * Migrate to gsteramer-1.0 * Updated translations 2013-12-30 Kamil Prusko Release version 0.9.1: * Support for GNOME Shell 3.8 * Improved long pause scheduling * Deactivate screensaver to notify start of pomodoro * German translation (thanks @linuxrider) * Bug fixes 2013-11-17 Kamil Prusko Release version 0.9.0: * Support for GNOME Shell 3.8 * Added a preferences dialog * Improved timer accuracy * Bug fixes 2013-10-18 Kamil Prusko Release version 0.8.1: * Support for GNOME Shell 3.10 2013-04-28 Kamil Prusko Release version 0.8: * Support for GNOME Shell 3.8 (thanks @haaja) * Brazilian Portuguese translation (thanks @aleborba) * Minor bug fixes 2013-01-15 Kamil Prusko Release version 0.7: * Support for GNOME Shell 3.4 and 3.6 * Feature: Full screen notifications * Feature: Reminders * Chinese translation (thanks @mengzhuo) * Czech translation (thanks @veverjak) 2012-04-13 Arun Mahapatra Release version 0.6: * Support for GNOME Shell 3.4 * Breaking change: Dropped support for older gnome-shell versions due to incompatible APIs * Feature: Support for "Away from desk" mode * Feature: Ability to change IM presence status based on pomodoro activity * New translation: Persian (thanks @arashm) * Fixed issues #38, #39, #41, #42, #45 and [more](https://github.com/gnome-pomodoro/gnome-shell-pomodoro/issues?sort=created&direction=desc&state=closed&page=1) 2011-12-04 Arun Release version 0.5: * Bunch of cleanups, user interface awesomeness [Issue #37, Patch from @kamilprusko] * Config options are changed to more meaningful names [above patch] 2011-11-20 Arun Release version 0.4: * Sound notification at end of a pomodoro break [Issue #26, Patch from @kamilprusko] * System wide config file support [Patch from @mgrela] * Support to skip breaks in case of persistent message [Patch from @amanbh] * Some minor bug fixes, and keybinder3 requirement is now optional focustimerhq-FocusTimer-8581be2/README.md000066400000000000000000000105071520625676500201410ustar00rootroot00000000000000# Focus Timer

[Focus Timer](https://gnomepomodoro.org) (formerly gnome-pomodoro) is a time-management app built around the [Pomodoro Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique), helping you maintain focus and prevent burnout through structured work and break intervals. **Key features:** * Customizable work session and break lengths * Screen overlay during breaks * System tray icon * Hotkeys (global shortcuts) * Daily, weekly, and monthly statistics * Extensible via custom shell commands, D-Bus, and CLI * [GNOME Shell extension](https://github.com/focustimerhq/gnome-shell-extension-focus-timer) for deeper desktop integration


Get it on Flathub

## Screenshots ![Timer](https://gnomepomodoro.org/release/1.1/timer.png) ![Compact timer](https://gnomepomodoro.org/release/1.1/compact-timer.png) ![Daily stats](https://gnomepomodoro.org/release/1.1/stats-daily.png) ![Monthly stats](https://gnomepomodoro.org/release/1.1/stats-monthly.png) ![Preferences](https://gnomepomodoro.org/release/1.1/preferences.png) ![Screen overlay](https://gnomepomodoro.org/release/1.1/screen-overlay.png) ![System tray icons](https://gnomepomodoro.org/release/1.1/indicators.svg)
## Installation ### Flatpak (recommended) To get latest releases we recommend installing the app from [Flathub](https://flathub.org/en/apps/io.github.focustimerhq.FocusTimer). Installing from CLI: ```bash flatpak install flathub io.github.focustimerhq.FocusTimer flatpak run io.github.focustimerhq.FocusTimer ``` To migrate data from the old gnome-pomodoro app, copy file `~/.local/share/gnome-pomodoro/database.sqlite` to `~/.var/app/io.github.focustimerhq.FocusTimer/data/focus-timer/database.sqlite`. The version on Flathub doesn't have the ability to run custom scripts, shown as *Automation* panel in the *Preferences* window. ### Distributions Find a community-maintained package in your distro repos: #### Fedora ```bash sudo dnf install gnome-pomodoro ``` #### Arch Linux Install `gnome-shell-pomodoro` from the [AUR](https://aur.archlinux.org/packages/gnome-shell-pomodoro). #### OpenSUSE ```bash sudo zypper install gnome-pomodoro ``` ### Building from source To build the application from source, you will need `meson`, `ninja`, and the necessary development headers (GLib, GTK+, etc.). Clone the repository: ```bash git clone https://github.com/focustimerhq/FocusTimer.git focus-timer cd focus-timer ``` Build and install: ```bash meson setup build --prefix=/usr ninja -C build sudo ninja -C build install ``` Run: ```bash focus-timer ``` The app will try to migrate data from the old gnome-pomodoro app at first run. If you decide to uninstall it. Run: `sudo ninja -C build uninstall`. ## Support & Feedback * **Issues & Bug Reports:** Check the [Troubleshooting](CONTRIBUTING.md#troubleshooting) on how to check logs. Report it on our [issue tracker](https://github.com/focustimerhq/FocusTimer/issues). * **Feature Requests:** Open a feature request on [GitHub](https://github.com/focustimerhq/FocusTimer/issues). * **Questions & Discussions:** Join our [Discussions page](https://github.com/focustimerhq/FocusTimer/discussions) for help and general chat. * **Reviews:** If you enjoy the app, please leave a review in the software centre you use. ## Contributing We welcome contributions! Please refer to [CONTRIBUTING.md](CONTRIBUTING.md) for details on setting up your development environment, coding guidelines, and translation instructions. ## Donations If you'd like to support the development of Focus Timer, you can use [Liberapay](https://liberapay.com/kamilprusko) or [PayPal](https://www.paypal.me/kamilprusko). Thank you! ## License This software is licensed under the [GPL 3](/COPYING). *This project is not affiliated with, authorized by, sponsored by, or otherwise approved by GNOME Foundation and/or the Pomodoro Technique®. The GNOME logo and GNOME name are registered trademarks or trademarks of GNOME Foundation in the United States or other countries. The Pomodoro Technique® and Pomodoro™ are registered trademarks of Francesco Cirillo.* focustimerhq-FocusTimer-8581be2/config.vapi000066400000000000000000000011271520625676500210060ustar00rootroot00000000000000[CCode (cprefix = "", lower_case_cprefix = "", cheader_filename = "config.h")] namespace Config { public const string APPLICATION_ID; public const string PACKAGE_NAME; public const string PACKAGE_STRING; public const string PACKAGE_VERSION; public const string PACKAGE_WEBSITE; public const string PACKAGE_ISSUE_URL; public const string PACKAGE_SUPPORT_URL; public const string PACKAGE_DONATE_URL; public const string PACKAGE_DATA_DIR; public const string PACKAGE_LOCALE_DIR; public const string GETTEXT_PACKAGE; public const bool HAVE_ALTMON; } focustimerhq-FocusTimer-8581be2/data/000077500000000000000000000000001520625676500175705ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/completion/000077500000000000000000000000001520625676500217415ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/completion/bash/000077500000000000000000000000001520625676500226565ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/completion/bash/focus-timer000066400000000000000000000022201520625676500250320ustar00rootroot00000000000000# Check for bash [ -z "$BASH_VERSION" ] && return ################################################################################ __focus_timer() { local cur prev split=false _init_completion -s || return # Suggest default value for rewind and extend case "$prev" in --rewind|--extend) COMPREPLY=( $(compgen -W "60" -- "${cur}") ) return 0 ;; esac # Stop if we are currently waiting for an option value $split && return # If the current word starts with a dash, suggest options if [[ "$COMP_CWORD" == "1" && "$cur" == -* ]]; then local options="--help --help-all --help-gapplication --help-timer \ --gapplication-service \ --start-stop --start-pause-resume --start-pomodoro --start-break \ --start-short-break --start-long-break --start --stop --pause \ --resume --skip --rewind= --extend= --reset --status \ --preferences --quit --version" COMPREPLY=( $(compgen -W "${options}" -- "${cur}") ) [[ $COMPREPLY == *= ]] && compopt -o nospace return 0 fi } ################################################################################ complete -F __focus_timer focus-timerfocustimerhq-FocusTimer-8581be2/data/icons/000077500000000000000000000000001520625676500207035ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/16x16/000077500000000000000000000000001520625676500214705ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/16x16/io.github.focustimerhq.FocusTimer.png000066400000000000000000000014671520625676500306650ustar00rootroot00000000000000PNG  IHDRa pHYsodtEXtSoftwarewww.inkscape.org<tEXtTitleEvolutionąztEXtAuthorJakub Steiner/!tEXtSourcehttp://jimmac.musichall.czif^\IDAT8}kQǿ/mݒDP Vj[.Uŋ(xԿÓf[-i$ddxqWsLFժ3EdHD8~777יUf+ulΊz^a1yttl8fct]6 OMAպJDYWk `f03e(n@&鄈^83NIF\UZk϶.d뺶1&}u41333&Z.(BP3̦~1"2&J`qDt 4aX9??տױ\5WA"i)\.'} ({wVP*!o)uioo 3{p8D* ,˂8mv3??9qggS\^q'cRjlhqrrazqqe:/ $\z RKRX,f_...̼Ǵ[-02"V5{Iufٶ}~xYOʍ2IENDB`focustimerhq-FocusTimer-8581be2/data/icons/24x24/000077500000000000000000000000001520625676500214665ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/24x24/io.github.focustimerhq.FocusTimer.png000066400000000000000000000021451520625676500306550ustar00rootroot00000000000000PNG  IHDRw= pHYsodtEXtSoftwarewww.inkscape.org<tEXtTitleEvolutionąztEXtAuthorJakub Steiner/!tEXtSourcehttp://jimmac.musichall.czif^IDATHMhcUϛyЀi"-bwRPǍkVDg=*TFp5WʸqӅ ԶҗA'/{s$LU?}s^^Qò?W(.m4C!,KhYkmβRj)R eYhZt~~fJҷ h4&B "!`AV1~7 -2ysJ)!5"juTJyƘ\g AxRʯB|R( Z?Dk l"zyoooq, 1/+z|)R1,^w] FY! `W]UfkO<3tq"ʣRjϏ$IlZzND=1ّq{# CW,}BxPjq2#BRB-$677 GGGparr333mi"Iuݑ"1 <޸)%="ʣ馉祥l$`~~~{ww8_m[\t֭0li?UdYh4ZRA}ggg1ƘOOOOc˲ :E)B^O(&IΠPB...ޭjf<}0 ,˯.4! mll\]]k=""aq:8H$< 8 !.ͩSSSOiW*r\u-qM_!/StA?C"aIENDB`focustimerhq-FocusTimer-8581be2/data/icons/256x256/000077500000000000000000000000001520625676500216445ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/256x256/io.github.focustimerhq.FocusTimer.png000066400000000000000000000755751520625676500310540ustar00rootroot00000000000000PNG  IHDR\rf pHYsodtEXtSoftwarewww.inkscape.org< IDATxymWu{^ ,Ff%`0#9blpwCw8݁v6db8$8Alɀdkz xzzCU98w[k[UowW3^ov1bBI݀ MhBOM&tDLhB1M&tDLhB1M&tDLhB1M&tDLhB1M&tSD7`BgTUtWi'40 `&'8=<} *1ƕ!xC{y_iBgd-EeYBxN9!c>@M>En1^1?Q<3C0gee1ƗB( bv F(v>@p"~ob@x#333Z#=+++!!(?Z|;K %z/ܷij4MD4> WWW?p.מ٤崺z7x=UUuLf{r1 i4u]#fix_OlDB:|kCo1^[Uʲ22<:FlMx Z_ ~ι 16CAQ޳]tY 2M'N'EQ, DQo2%EǨV{L 16rFԲ۠uBxW]׿4(]@eʲ+Ҡ)1b|i{o|ZM_/k4M1;77܄N&?1|YFoE|pL. "q)]@o]רz/ͽm=xh" 'N!,'Yϑ3g ߽V-Ý- uj6 m#T5fWfg!M@ fU0rҦ[ qp~5jwQm(c0D]wEc33>mNhRX]]@dsߚg>o15P`w"1Yh&jWgEh"H ^Y1}qqѳK ,щ'. !Na>h|r'=_0nC}llu.Ⱥ69#HR@Bb 4vJph}}u],~/„>g'6vco:FS\;˘ _>2LS26IGpsssLh410UUuh8H joo6ο], kˁN82X}&r,8D4FXȳ}E~waӥ7mb:IXgWVVB^7\c0r99䐞jSFY64 `ާ-!fssM߿w.[: 3@KKKʲ`Y/z9kLQ{j&ÂsאuYD ~J gC377͑/8D&?u0|kz^oG[m;O55~^zYE{i ,wЦ7_1r~~&4hee@/UջX~.(&L%&(q#+`a4 kK 8C4z!h&Տ8.eJj"l&6Æ0L=] fST-ilnV}ٖ/4&$iee!7-r2}jZZ) 5f|>gn@ 1+o4L+0bHu wF89ZYYSӢ$=99e]nGq'9*lۧf|:f} 6u_`T/j m:fBD `(TU#_TkQ9YL= åΒW )o9851%XŒu]ƹF&`tĉ>QUBYd3V>6F|!#F1;ܦb#h4Z򂃭%9iX_`iKWM -..>OUU6Է5֬>f ﷝]z,MR@oyV&ZDoC!(s~~ L`[ӰXz-#S,2m兘I{bpw}F.wcѼ :3>lYjq,buii´W&$4BUU^]b[˓N0<11O4 B* <Ï:f8aԾm ӜYM€ʲ@UO1~ԖyD)(^RU=yk.?>AdÆ[2O9 ]ϵUxC"C(nb qQ/h= EQxqqEŏ {yF)f}'`z% Y*AGԤmt߬;=Bxٶ1z\BɪK] B?v7dg4|wUUbc?H;E?<6JxFM7 7zQkaE%!>w_F׶=\Di{m:󼢉 Z[[{r竪1Z)mĎ`0YK){yHwьBn3`Ä69 }r{P蘟\gY{*Wj`,`_;x'c^lh" &}N\^,//_ ۧ.)r˅6@lC'G[fv1L*ƵC*TlS L.[.VV>ADu}ڲ[q:]c]"ڃ9yVM5*?`t͙t> k9S0tA?v?,>x@,$!tB1,Ȃ,  :?+ew^!u Dr F#̙) Q1 1K{4]x$ʼ-(Q\\\s[رcWzZ (3;T (X~&CoyY4H ` J+P.* }a|BUU7e95e'ϳJ33y =*aFm 60S}=H0c2~PT [ (+p^ ׅ2U1ĭ.h9[϶==׻L^= D:c+oavgxks;ĉe۫Q- #oq˃; QVց7m*Mp[GmBDC|uI;ڨ@`pFcb|6m"6!cMo5'L(~HP1Nu m5|ӅZ.Yh|Cb UE5 F,//suOCNLl}Q~0>A-iihM”rM4-ZaPp9tKê "JI{-`]B wy'~.y,--pT $feь3*?*ŗݹIO?Ni͙oiFuPP={aoC95CW1 \o߾?: UU^1cRÀRM>yY>2KԄM=& [3kMo3,Vx N: g8͛NuLƉy `uuVUoI%6辏JͅTS9K1Y#z*D8zg=g nGƌ< u c5hi^pn;cL0 ~(= 8!քފĂĘQڝ1;.ܧ𾱿O0עTCtST~/lP Ϸ5Us 1>_|(h52Be~n'c1tN[KKKO~j?"q4D8Ƿ3Nɯg@%L}m1m>( šg"{y{css8p׈!{:-zUO#AJܞOWryb] DTZ_X(xlú`H Y#8Zo1DS!`Ǯ[(Gt9AxMQ~&[>HMvxi3#lG 0J+LJ0$,׶() }TkDACIhkKhaD.Yn3\3q:bm0l%~\lBx!yEQu]s8~S˲W2Qs~1~ 3C%{9jMB} !b`.OsSޛO%6%(h r h)(@`0 օs(MeYVsaDST_#0{Ea~Uak_+H\wK@(܀hon,,XdZI7>EZC>vaE=!L@Q1PcM|Qigc~;m.֖hFeQ\_4 kUxq_#E:4M_h}rɹ@X@\nOuݠժs,Pa`Gv\?.1h7ASTebmYE+=Ǩr܄OC f|{ ^١Gti745O/9<[yW r!>o S#a )Rx-n3frxr-G@Sݛ~Bn;ef^z>B"_CW)RrX |^p.;L&[(UEEQ,ˏ= X9%?-;* t⥿; aA"ܬ dj[7-%[࣡?̌> B'<}%Z[#UnD}.6D܌3ۼ3JO da9a`h8@ZNFrw~gǼed^4XʔMYgu]^MPm'L?vS:g@Ya62K,Y j: <L/,^ʸu \^9{XK]b3  7c}~k{M#Vf*c|4Vt=M_#`VXW%Qtݶ!`|ݨc]x5_>Vy= I 2#YZR,'"0`6 kҫicX,Uu9@XZZzMUU JAz4\<] e4SRj?0zH?{gmNCܳ&DL_('x@k!r@ɡ <Ÿ ِ'mUoO=JA {<@Q]^/Ҵ_сHP kg6>4G}rUU_{A/y}AL 9V`@lx };Xl!fjxsmJ]8LL9'U^gڄ''EQx8: ³Bq'1(e <؜A=z6GemEd]~V!=AA|{~յ"\(F(33[W3 AB@ X+e)Ci&yՎqӹ ^p̙2?cJ!6Z a|kYvR'U5) }jMJVr_NCF!yhb "F̓ؾ @Txj 1rQBQ׵Cu]D$0^~$1v~fAyVX' {y+ěH}zq$AeM|6fiX$dڤ9 `>qa\Z`xiMV&фϸ 901)qXh|NdEb M *5Y]KhԒQ;:&kFKoؼZ,8UD#8m[rwT` vՈbr5э?.l%тBAkBȓj@`rtl*g{vuZkQHCX$bV!o4.z6& "a&D^EKkXR"t9Ƃy+7غǎ{JQ} qVI52(ѐ<8 nQz9^$LLl=$\!Y,贿g`wIns;%MhxCn}>ȴVf%-(&b *djP!'q4@Q_e ,ZO2Ҝ-HG`D!1i&7?/5Uk[*8>V}^0ڟ?ݯS?vkd;Gm=x9Ɛ'H// YmzTe"F֢z3`:ovt][y!&WA^{ϟh= Z3"o/Vs_ڢT5&@9!;?[{3Z:l褵@Tƨ}caP)!\ך\dqWl1_w-z+ZAF# 6 '|o3>?[9)CG ʻ]tm=+͵Z_ :&K$۴ʱ}bV.?OFt|j;TǮ*$Ç_Gi ` o+.̙ ihb0&ZĮO!DyjjҡSTe3Ѕ@* ּ H l -~4Ca>-V>GDʎ?NB=e\v3h%:s>|LkXehN:/BL)A"?Y@0v9z D 5,Ȥvglo'k?(/Nh0 ɗI?.eȑsϭa׺$&Ckm!r+rڅQuj:΅Ob:fgsf jZ- hh?k=2B5|Ou 663k Y<#  , cwЖf!jA_1J>vBQ}\\>Lz 8I ;o%饩6aScN K--˃ޒt` zY]poaX2G e0^$:W `1&4V@Q/ 4lXD^_4y<53HMl}i7W5fשQF#_j/[]҂1BË!_.(:%t5cDžb&kEj 1~;ƈJx!>y0bD 9:4D=r+ibWw28l|xep6wbm B4 .@ L'xkG4B/iEbIp_F$Ӭ4'B"놵Q0n9k.X+p+3IM!*]%ډM_t5ۚ4Bs8rnDC~tցw1XkkYo߿Ӹ2 8OY. e T}fMhmZ ,ssGu(ܛt1F-(lR`糕BYhhVП4`P[b htl‘.JΖ Dl1nz @R+H4BWr2Gkv1 |Q4z4 Z(?Kl@M@dZP.>jsiq`x O<ǰ9ϞSd,Rm̩Yy*hW *#{:yktW? [xC?3H50, p^]9'mqܪ|4&͋b|܀@5u`4 3eQ1a'2`+~c0AM5woC94W (X yÓ,h,BiD .O0Rgwu5*1 l mR[= 8B c Nz1a_=cBc#b#s!?TE~2:ֆ1}h-aJ!pʀߓ'_pnzAL+{R0тv61UKW6&Uq4ć@`(q"6-;tVok|ҨO&ې]Hc#xM l5_&b=bMc&E9֬.nhr1@1j%8 .=Ć :BqZ;Oޏ!G]kٶYCaVwL8Ƭ8H3Iǎ[!qHj Y`Q~59S ˋxڎ\'o B'@/AB!i,EDg];lI4FЮ,  c' ΕOf°y]a&ˬm)*yvcj[uzXȹ6ahv W~=PЩ%  pHOUf~յhY0"c՜-4 )䵊H~WF^{wjU(EͣaxŞe:6TSx?8cvjcpN ]PfSa[~kHa4&ش['oI6yLhAb;Dy!aCYsXr{Pqg,;+EJbGsjaטbM:ضOyNa)5GeEϋE r!4TDĻ'=/M,6U֌%]ʂF5w1#7wn̳ x,h bl0==+| T["a$qZF8 ĚLӘՇli;D+JmfO~nmGd}g1`۠:;;m'H^INL˦El9[ l6& M0yz.^3`K('2 *m{pAOUUoWո˓O?oo'X Y:?h, YBY=#%+QAץ^ʿlJź>61v&t)0Ҍ.6v7wsDH&02h,G*L .u#i~uW:򗿜}`0g>|_̿u];$”cI:L9N$Ծd-Kh Fӳ%kaQ{=ky~k]! Nw,}^+y@MF9nZtbX_Rcj#y]yH}{oy[F2?~ߎo~Qc NU@%dM| lLK(tKmz`xA5غl,ÈXK3u!SkOAw8ӮEg@p@AG}>{?_1f)Օ09čI$vm8qDrwmm ̚T@0˃/ZP-XXVCIE Y<>i3\[JIwLhk=~W5+ENz1%[W5EQ]w-%m!׾pA</??=﹨ gw kg?9wy űmOu/v]\` VN ه<;Q0Vx50LD}cG;@[Kh By3;h69#6?\&f鯱f adƂmDxbMMMg~_oY/_zz2t/~ ;я~k9. ޚؿ?^%7Fl+69 {A3[+^ zFamh @6ᶳ@+M'鉶񀐝(jTj+e?X|{ c /c <+o}/+I~UW%v ߿c]N޽{@(#"}c %ICs MnUH-?-?iY3,B (xsϭ؅Ic?]F1 LUpAM[?{Jky116[$2lW^yeg; Y---s-ߚ\)yT~?ydi:`0TSuڱEBm狸R=[(U'RYzK/EUIlH4RN#Q{ӂ##:P"Q*X 'l{9/%iOQw ػw Ǽ w^?~G&_fަ6_]HŮA~ rjxdU{8وPJ29%oY2YVcc1<|1{A7`jjα&ZAV *-B6^qc0#p svIС/|u]\ e/nV|#1MOOKIh_38|@M)N/Τ8!bV\+.}z Fij7&'\Clh?7i׻Ea;?~;+ ĴNҚj>vx}ԝg'rr&kIs@hmm ԟgg~. r+$ 2e~j?]%>4nB{@ցu_TLph0H;رcYf٩KMQ߷qdžCW/,^'#Ùu&j5*?n腸ꪧtôxRwW4 ɚ1[tD}4sBR0Z[DQZj%OBp?'vJpEabdŀ k ^GWRc[@)[& B^o6#ټt1iÏo6}~c-h/ڷo/ZsfggG&*ty\h@@1J>HD;Bb&AV!r֢ a5Oβ,zݍ1(6@(M<Йh Pr8)i|Z_Z헰}HT3;;gqw`j'?)#U\#mv-~ނt 7JӋƍ݆ˠ>,05,иc/׾bM_t)` J(Jv9z^ dϻvOCD}0܉BNrGGg ېSmf_Lj۴>g}^e Bpﱌ9E/JȑZDˈٍL*L JK# z* ܇u5[5utg#@i*gq4{`ѮP׵T&?bz&!=Vad#.,(]'n?_L۞'u; L/d,DY0WNF3}f&R|mHRAC}lGKxՎhTcI'2p4 cR/--%& X΀UaT Hss2  o :x7[-߂-‰] &UOW1X^NR@}N[Fl XWL3 sۈ:IVeMKb>Z1J{i5"C N,ӥ334Aټ_[[pw4ygcjic[%/VEc7+&feoWسgCP#Qmmmmxo']>ŕW^0  FJҐ.k#m͎]/AR!4 ˪ h0FʲƥVF1SS\Q±:& ˿K^sSs+K7^h:<:O~Ŀ*o~%ٴVo|o(^pF3ﲴgggԧ>51}eG¹@~4wR@u 7Yr^*XHzwlρsss(p4) iBUU|&}QuMTU #lA]՞7|se]}x[axoe?DY+qE{ =66Ս7ވRmdCyl &%2y5`.cg7RQӂEَ ->fff4цhv)hCMS^;77c8=cI.\lЉ[sTX9U8gL4}39xӚ IDATmTTk<]QtWSG_M{vď,T,Xdiw ʢsKڸw `Tڠ=4 PiKxM#sϏFS۱𜯡fAFll{?l"`M]Jc!|]B+eYbzz~=vKKK]&M_/7`~~5M˿kyꫯԴJ2([Ld dZ>tA(^;3U/rxxK ^=#D4FAbp.gbZ Pp?z&g2m^ AUU LQA_5ʲ=_xE@UU 1VQ~cc~0 p!<3 LMMe&jCScrX!r\@̕Vѧz o^0@>>$'C ss-`7\kSj15w-d{q677;pc~~/M=h,@׻G|}׈@Yسg7x#!KKXZZj L.k?ݥ46 p'l`eY7K\wux3}[zӦ.Am%|nWAlWk84@Me\5 1;;wjjj֒z1 ?}xz_|?-Z Jʻp?c]7bX[[:-a|2OĻw!/XD0J T;Ÿ뱲rG:ױep?(uY } mnnx'+'?Ozғqt s(EqM7~7=9;;݉9i9oLL+KrK>?25s>>f5XA`Pc}} UC`M3ڰA 2]HKil@_uG%(nnʲDGVWp0lqq 'Ntr pwo" ׽/x xgT |DVs9j]@-9/ĩ6wn2{i(SޅIE@ 83$9!(f~[kZVhm`}} ]eYݻTgQlnnv񊭄&Lkyۚhm@-(gs7㮻'?I}XYY#<,1;;~hYyF;#嫫X^^&]㢋.%\/^x!.ҡΣ~-Fn{~3kiz/x rLqwQMo. hc]vkk{޽{g0`3^̉Kw1vV=rOx0VL6VۚEZ]<\wuX^^ƱcǰzJ|aYeMR]z' }=8|QϷ|:~h X`4fiաƸB-(CPdO kYl_|S1UUF4]5ylh̦w@ 9@MYXON!x7zػw{1;;-zt=ٖ.f7/%s}>=Kibԣ2<ñ.KZ9~79g`uURA֒|ss9G"ڼi4NAѝ6n]pMc?_+k6N_sևzm06)l03ӺpΛf7n\M-8t!C>tBOGc%ʲD h[KA"$H ^p 7 GyN0-//ѣ{ ^op{\v>fںO Ib ӯu8yJçf___oƆa0033={`zz$W$,#n@ "e T6w!طoǎ}0I֬Oh^ﵜoJn7(UUajj-t[ {/|7u82. Ĭ kkk{{Maž'`Յ+2+!4&Ӯ$!)ESڮk]!'=k ??iJsК~|jEQtZ vBѥ=jkhzmv}45666ϛ.+Wcn4ߟj=Nܫ1lt$عvEB0LG=p. WjG1} 8 Gb?H5F g2:8kZ-YwC,qvmٷo_wߦi˿8t~a|k_<|+%aii9[+ƈCup",..u{S_Cwji<=Vw~#WTZmu] 'fwvW!Ă]ЙI5Re6?"g{gxn<4vфf|ZƆFÅ8֜Z,Zf >ɘ ըf)n {1<8zheeOY^^ZnyVf B-hLd&11$ʲ^(*TUSii0 u08V BwoVadC.C;ȻNMDG<^v)R9X[R>)2JQl|ZJJMCԓbߴ] :FZW{=b}}4l-qnTp p#&t}B !TI a@ݍe>gSd+ @h]L]Gcg?iei` 8=R11I;kABY>(uѹ"L{/W*Ҷm;qjӞl(R"6zVK0.9r~wɷ?vL o-84g$v wah4{=oK}fN t].%@~ٰ&=|B *4$#a=1xk£bEЧb<"LR xUٿg$'FQ?6qߐ5yr2g11ǼeS!C|VkBݲ N Oys^ 3kPo[W?w5Iǚ*Pױslrgk,7:%bG ȮMi_!-ŚV?0-t0?3-k3\>c>},bM1,iefuXhx 5-o-\o)`NdC= ^KsغhweymYݶjNsԬ2VMeSS^Y{!"fCA^rETrٝ VVe@P-}Z3b_8Ag'F"/#vXʁ삔e/ndr[ o_0m1O}D.imOė泻sKfXH( H4]-2M$Uk-̴ /c.d @$O=fbrZ&I 8yzؗK:}h@uهI6_|ek6nA7yGSj~'G 'h g[,|&_½48I6$+`q8ajE@1f¨,\Yks, 9DGxB$4`~~pSSԢl< nI4WT4:8+C3jk{Z&"$V2"}8x wayy9=-3RƏlɆ[&5;V0mMK k%E QLW67/6&4@ݔȖ!7URn]VSk(&~ݻʻ OT=n!㩯5iXթД5cA[N6'_]~tmWwNcdSKP:,ƴcޓ |ueF$pX}Hs$<[B̼m6#d-?k;`;>k{ÜlRN2̤]C}زsE0(P<*jE{aaaVy{ s\)tSWcmhh-)njhh^D^z2A_9|QNJ>p >'0\VK Pˬ|Oxv0[߿߇OFX[a`$k \X[0Lo5M3(ј,mYEK ʄb웫eX6X2Xug3Tnll~X^^Y]x׻ރ%|eMs.[7P>hdAJGbd ́\ZZ;ftN.l%N+cf:3Qy*HywʸG>-y |̘EB(=b1w_vQ4]4&vf XFXxReޕRKJj x_Xhԇ7ǩVtN+6f5lXQnj .FWZdL38~gwo~>ULqvYZH!<Cr/tR;v+X81C[cgq/Bq,wsǀ07wޥ1)B[>?p7+ P_HMXY_'Xpꋸ?>?uBE߁UNQ+@-h>`I5]ëױa˽e Z4?MaUۯN{9toK1 .4hL1=kxggT_lUKo :t}{3Zlk7t3NXN>(*H`,(ᢀ+1n zZ$ ']i].ENsw9S`~@|khP4=x\Mx߯{lk'VXYY9;z(~ޛ)[E#^C֒ $\]U jd1~#Dn<*bïϧ[:1SB(wjƎs° p̮)ylzƖ0e4.֘桼Fnf|8|Mhs4EӫyFD.vt/k=s.`08¢xi_=>t c1_MAGIvMmLVߥhMo ZRT&0)+</}KgnqJp =Hr:M9A0/k(Xkq`2ƈ!\p]D|l78 &ߪʆXC[Lj&s7BHeiu\1c>3Wi=7ayyD71T f3K[E7E& A~Taåw:M G>7Gc]@(dT#<􊪚Ӷ@$LO\V 9F-EJ9^eh=&y݀ hZMR+- ᵷ_Z"TLch#\vy _c#|MvG[rbG+e3 {:|?.xi;Y<Vo8_4o3\9rfj( -尧"0<Х@ f'f+.S~ K{ Bߣ@$XeY@m_%# vr?1.d!) + ȇ l9@?(G :a5,&3[S!N7wMv XAMnb1a~ dWfMUw%-CQ'666NB^N^~_qfi& T@RCS]P7A# >#PÅ(\*(:5YCA]>n-񥊐fnO>8w*:AZJv6 2T,ǒ PU&K.qV4S75Af.RakNW$|/67*67k<>Me1}H8jDtvs;x` wt4A SFJxLit; ,H"5*Eˏ}c9vH]: vVwx3-bN[Jlzpwf| `F4a~4O>&aM!^{ .'QW0OVpUYC~Pnڴ5z/{u۰]c!I$ IDAT<~S6jIٓ& Na bZՈghsp]*+&yP `YaeMB#[B^#H+ . kjʹ9LUC /^?2siYOp:#{G}䏊^s.dFE\]2jb$Cfo.i3{脌F\_ssƲ ƊHܟT;SAn Y5 ':|`ndf'"xB-d@{>y@I?VR7wѦ Ӷ1pLNn8Es`-WZa 6a!q|=G*"*B2?`C+| Vp_R|{{v(m >;gſoMSV4rk%Dr rI,4v J3[Z'PPwD2gIߤr-Y͞_sPM?Mlpgo~yşQsm;xYg8M}ΝHoE0Lba. '%L,MԭuZ{#VhyC7(Kpd4ӗ=lS)c:L,?aTҪg'/GQ~mg1lKg8dSa|s[n^.j<돳n8bi*Sj~f-rd-NHB^[HroB/985$Ep[Qߵ,Ke?[0M!pY ?Ϸvg|0},AgZ*)]vy 3rj|6no! ^ojC'ʮ8/"黼z1tipeN6xLua ģ9o{n%0[NoIgKptv~[ӟ|˷\Cʅt֜)-v}YkΫN5y3[ jd@L{~aF+nQsCi2;b(ꇟ|hO1 'C *VUv=?r/1gw /QǷ!882Hf };Ѧie?heY%l .k5&&/ ^@X!a҅JنK4PA$:,ٹ ^V|k"a hzu_O[̛̹Qsu|K:|so?= S Z3l[ L}| <-MuEl8Pw8BJPl6+w-%Mevk␽F~?=)&aG9?co{ݯO;xis^l/}'>h4˲n tjEp0͚MҖm1#>ś Ql)0NP"-R Ae990G\\υƞah+~wѝ́}N 5KL>AVG~1B[tX֠pg+ yYډ止*$f4os^>'e``,"qr]V(M1!c?;<alt&]'EgyGxxk67`ִtHKz4Fi(KGv,ޜ>"|N5ǵ^jii7k\1Mt [.n +dg|MYV7~wUBnVsow?w 1 $NVg `Q;7ҷ}NY.)5/v4-^njc"m=t <հd e" ~[U7]}U?3>KtZ*![cBH!HSa+1EkB%P &4jFjch%*B$**K]ݵ?ܹw}ww~stSXg2Yyv ر04Y(HM helЃsew~ᑥKȦĹhm{ij8(#>(nJ^O@7^\0om0D_+!y|\p봾hhaS\jyx>Ǐ_M$@d7ކZF})kH-ʱ4:A\= @/@@eeyŹS A47$vտN?_R m)6V36޲ yO\ľgC^h}lN>82ol?)>9=S:eRKR 4`9h<.իWmO--c/zBo 7P(`dž(aћ-duU&Q?Ԡ0 ud аh"oΝ_~xhaSXb^!B2f<̬g5a`yLf ~_7n-[رϜux9 EUq un΄p낍&,/u4sC [(U̲;amn/}뺐ĉٲa]b~LxiTZf @- ۷;9p;+)6e_)k'47dUL̫L: dk؟OzBC^ѧ|oLug] nN@](uD`62G}t͛-Z4 ~tD}[B] W7Y^5ظBUO COV~Cgpaݕ PL{p̙O{e=d)L|ߗ@Z_|J-x$h\A(QF2d{zk7lK,9Z }ïvXsV*g69 ;dq@/ vL A׀?==_m߽~bz cPUIu` )@D8Mw3 hp@F.dl_iӦ+V|j8=ye_`JUzkif ]'NLן+}^y[x{Uǎ={=ȏ̔?@@UFQ#hD +8,AiY:,`V7޸b۶^qŕ_ gǵ#ho<Ņ>|AQPu; euX]' slu=)ۛO=W!ܟAE94M/\w@ `񾾾ݰK/yY@v-Z,m'ު`pk@Mx=oBR)#/_xCSSSgWiYecl6aФI{  dH@F[|yn+W^eW[T&B?_y9 #@]<b?O>er |1 Ȏ*p584TC.l,P"0 h# :fH|>.\+]겕ys'sPq= =lA:C/k׮JS ?X{i3j|=mJ) h\~ cRR$t axҥkK.sw01QuXށɱG=`ߍNLL)e*Z䣚c(c}[S_f@RZ:d␀,-c$<\\vW:/9,_lٲŋ/;\=dz|6(J'O?u}CgFGG@ubW+,vyT?iZd@cH@@RX6kU 20n.[,?88x֊50p^o z{s8055y/ʳɓű|zttxIt@&W%d0+iNUHM~[?)iؔg4 "@ (eCza>Wvj*d^xJ*_4o9 @0Ma vYYW dVCH$F0A2W:}{ xԤY##HQFTFi{n%x@t2VrGV7dGyD j cE`EmG³9/SS.lRWumUZS_o t54I!Jhѕ*KBE6kEp@ Ud-`>;4۰*.^4Mwm#I&Hdq )=bx ۖigU> TC Uoxh\gDO2ngTӽץZK# OP눀ƛ6KY 21i#Qt"KL'>MFE 4ݖmt&wR`B|2o}08JJ R~mtGU.V C3 창$ RVUWqNO&|8 %qy|&fvP؅qqZ}EY]!!A^yO3mIaVQO۱I~Ui6yjԄt 8%6auulL讥vjC665ՕMy6u"K5?@x#2umml*&ʚ6n}S$𙴜4Ti`CQң*_'Qhl8 Fi5 @:$6e&3iר`LhteS7 7MҮM}Iڍ h~MO ]UFu gHf?=%&)<.g*kS?$iӕI  |&mOXR&Hl4MI%΃a2X`im5[JR$Y`"FhfAt6 cМ'*-"fN,Hڦ]#* &ʤW+%*x>54Sx$BL\OeB)~RIo2 @&m@ :isVZ'2@isbJ ]KR.1F@O.]ht "tx@IE%]@ity%tekQ+]JJҕy,]JWOp*IENDB`focustimerhq-FocusTimer-8581be2/data/icons/32x32/000077500000000000000000000000001520625676500214645ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/32x32/io.github.focustimerhq.FocusTimer.png000066400000000000000000000027061520625676500306560ustar00rootroot00000000000000PNG  IHDR szz pHYsodtEXtSoftwarewww.inkscape.org<SIDATXWOhWޟfXH4`#m!B! ZMi{bKPzjĄ=xHiڂmjDӸ;vםyzpf;NM7,~{oϐf_BCDߤsl\l+ J#D)ceY> QJHu ض}ƲR D" xP*|21ƴG%c1&T-p]mXwe,@ADPJ{@cDt4HXɼ2 A4*8أ16&D漰Yarr.^߿'Ol!F,vJQƹ研BPNwC#ěqXq0"ꎓ/wqx088 <|JQ"A](Jz޽{}6݋r fnZh ]M,--aqqH&xә4&&&P>n SN5|߇RƞZq=`yd2Gamm L_?q!h<jZkر~Ȃ@?wލӧOömTU>^ c ._ B@VXYYHعs'cUد*؏8x"BRӧOh4X!d2達I̲1&wDtS=X BppѦSJ5ֺRTqHqJeU) ZCkvyAceYeeDDm,us^i7wƂRJ&\J%c\{yg 2maaq^`y]-oPԥMh%IENDB`focustimerhq-FocusTimer-8581be2/data/icons/48x48/000077500000000000000000000000001520625676500215025ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/48x48/io.github.focustimerhq.FocusTimer.png000066400000000000000000000054401520625676500306720ustar00rootroot00000000000000PNG  IHDR00W pHYsodtEXtSoftwarewww.inkscape.org< IDAThZ[lsf֗kq]a0*RHҖT@EUGHZxDG M RZQ%b;i]wfӇY׎CG:|cR _ADn+xU`CCC_kjj4!)D4 @_&7?޴aÆbDt- 6\.wƲ'NHD&ZߟfXV>W(8Jk\Pm+ʫ\.7N߿? _o6??ʶm%,!"mKٶ'GF>RȜ7L9OpΗPW RA) )eҥˏ455E)UR,l .|7y0x"}?9)ɪ# ">::zKmmma^N'A<G>X2=smDTZΫ%@D[n]㫜b>pJI^USӺÇ  X&]EEƸo[}^Jg}<7RPpL)e/ײ ">>>~a?tOBn3?99USS c X}t- y)Ug!ΦPJE-SW ҇~Ҭ-!_{ VNzGYDN:u=^4Je4`CvIPL"XCCn)i,i}ץBR ^  |Tz`J+%Hg.qMD444EH|H!5kGdôJ7{bmwރHgac wN_Dan1Zw,nǁ]_[,cZA|u|.ӧvm(G6ףnM@.jC8׍x{^ey!h.KP(`_ _~9]v Jlٲ6m_VļBٲRP ,Oiq kצ쳿ٳcV͡ %"ZZjA)U[iTU[:T*)?ׯR<:={~aǣE,jH)Uq kN[ m۶JIٳ3ooT_?hCux2c KZUmۘBWoLH&t`zzګXŢV 8!s+%t:_ RJ8p.Lh]eh)bll =S{##g?\-taznXVe4͋kdfsnF}}=p0 dsJ1rm DB!B  ǁD*X՛J~؎E|Du5GGGm۶!pGOڂ^x~{i;Gbpp==%2 #8ضJ"O@ DZ!$(8WH@rsVVVAJD7sn 0 uuu8;![HӰ, 333RP(8 Q]]Db***a, JIl6ݻwݻZ,p8gp|3f3-$qbffT X,2Ҥ>q!Zpa٧u}2+"@D!k"Ȥ@A#J*˗LoH二o+ q^jii[eGJ8gN"M@M]tr[֭wG.1cx\S58?P.TyK篽zK=cw® Wso&s3= T+(ay~wyv׮;ٹoͿY$@w>c ?hώ;wc PE 3 |[V>yO87  Bd3r, ^,͝"To}^D [?vv޾)OUWl031==}O>/d=_@ -Ij5Rlk*cJ.^拾g?;\*au-\4c:S{] #=~dzEv(<K(ABOT#I? ]'(Sh)XH)RůAZi^,BRe[_g+9VZX/tIENDB`focustimerhq-FocusTimer-8581be2/data/icons/512x512/000077500000000000000000000000001520625676500216325ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/512x512/io.github.focustimerhq.FocusTimer.png000066400000000000000000003121601520625676500310220ustar00rootroot00000000000000PNG  IHDRx pHYsetEXtSoftwarewww.inkscape.org< IDATx{[VZ{>~niŽMѥ{vcƘc3CGGGGGGKtttttt\Bt+]踄 @GGGGG%DW::::::.!q Ktttttt\Bt+]踄 @GGGGG%DW::::::.!q Ktttttt\Bt+'݀M> ax^0[gJ)8ܷJ)٫fvrX8+|`OŬ;:. pppq?;K)4wxF/=y} ?]Yu~͛?}x< @G7 ߛͺ.}`f05cP /BOO Cqcׯ_)3]88<<|bRʯ2<$6>Z=0lMs-ӿ{֭{8O~?fKF0 e02>yYe`l 86(l؏r||77=^ut\6t1Z.AO.gRTo{rqq `mK8e/R;::+;יٗ|ŀR;r[“sB2T\75LOk&`1AXI3|?pƍzHH @GC/\,_KWn/_&4zd`+xnŀ.9'kVƍ`d'0еk~;Q+GGGoZ,_ k|I2`Xl}4rG-NaBq?{>](92h8==Rʟ}aGǃ+ xv\AӵabցԂ}K)*a`vA|[gIt?W@-ea8=]M[ 7_ׯ_XG%BW::vݻ̾f t >Tֹ*۬eV`wA sc>1ct|+ٔhff͛ott\Ztcq_p\o[j%ϑTgQ[4E\L1@UpIo|(]3Λ7opE999/X9;CS,e\O)TABz=:}hؖ(فzlMr[qjvR~ɇ]Xܽ{x76f, x} 8rKSն}Ṉ -;-WG#Ў#ʎq̏@GAW:.=޽KqK)6l\nɽDov@dW|ҠZ1m~-ɻ;f l WLr#fmjFq7oNwt\tW_ 3CfU.yoGKhafjkp o3-]- \жvQص`_E`OOo|lG;:bt/\r[# ;e+Њ9u}mz;ЮK/}Wei؅0RJȂ[#ك7{޼ySt+ w36/bgMU,̷ e[-u}X3 Z_ǻZ}h 26|(n3ql)^~On]899y/)uᲦLs֎[?oխJJu]uv/xY>EY95p?+/?Vt'lBV1w #P8֭[;;:.>ݻw̾ %a!V1p~9 @xL׹\ of:ꭇcb4O/B?3'!J"LjbZ{^o~t<3̀bryB;w;gCmЯwnS,zVfLl<-md*گ+02BvkLg][:ɄqoxZo6'm/X "y~pXA+VBTh63 ,}\80ϛQ+M:`E]|ftQ8Zy{~x>( +O๵տrbUᙅXɝڟW ٯI0 ¯Y19e[gT@N4Rt) |ˍ7g %qGGG6 ß؃apTm$Y :Re >QnE;beEwsd=Ok\Qx?ay 2+̃Mc3#]v; 8 @DžK)-o5\90D~:9m*!] )~m{)sn>10~l>09&`mZmTc=Qf5<ͺr4sfn%wt\ t",.qk)cG-t+c%g,dVnzo>(wk0uǥ5N(:!s\jq#8S[qq4V{n<[OG+GGG\X.X.ȏ~4`E`se@\+P Ck̶?6~L6M*˓ў{ VՆ JO2ׯ}檣<+ Q׆`Xb\l~41C(/uY~BY_ճ85ܶ-c FaB qNuSW2_G[n q['''|#įۙ|*Tgmqɴz*-A V@Yxlu:|}kN~^7g't\kZ P p_,8NQ}k}?8@bɏzb|6EetKEئ JHn0n wHYt]+pqA+Wi)߸qvNVGFW:=NNN0=AݛȫXSHX>l@#~b}Q%T9Z BnGsc=~@RBrAYc$wYUC}G vs::ZwttoC_,zJ Lve|q_ 6"[lV=soiaR<(r~$˴.o[,T:J))i+29QPXa(+Q[^;NZTJ6s~ _:<hgcF1sSfy@C7S)@VpH1!h0ٹ`)Q[hy=C ^7o3M\G#DOuܽ{0`ڿ=l<|!*=Sf}1)FeQ+0`w]ܴOXA],^,x=&㱠+w}?gx?(ϬM +T 񂽏eoa20w-nK@,y(U- P :~Tʁ*vBӏkɰIqfTccnE ઙo1a]x(w rӵs%ߢ*|mUa_l5{9._G΂qk<28跫,Pۊz_xf\m< bng&X.K,Q_|ѝ?ߎ'$q&؏8 o1kvf-`Ѷ9ز=K]"Ym CȮ$mROpT}׶<?KCKgwpg+Pj n޼U^=äut<4t`/ʕ+X,"jO K{{~B6GɷRIgA\(yk_ ifF^~%|RgKQH(8`Ձg&^_7NOO}&$ǎ7R~{ Br\(.*3aaDR J>_u5sߏ 53#oRoܸ=㡡:+6xg:ѻw~RG.ǂwf>f?lUolm{<¿Ag ;;Z?˽gmK"@MxJq`Ib>]$чV@hKƼ5cUƙqj\~g_g33g3K)x晫?W  $J>*2t^Fl[J kuWgo Q 5׵$_&$$1ݷ!؝ D;E" e ^~Ƙf0ȳޚ;:=]۷oqftW_}j5 & r3rtidCsuZYH,s>mkTdJǤgP4joDݒ~zW%Vbw[* ?$?.̧[ q(_Pr<'W\{~bQ۷uѱ ^jP^F[ߛl)[icf7GgC "ǭ?r%h( <@]| [Z\WKP 틭zq6uP@X"lPp+u?c::Όt<l,uNtւ-qs} -,۬1]n%P'jh͆0ADDJZB$ @ssa=,]-+YѮ`|?}53tsǿ+<ߘ䎎FW:&z}W&a t} ZH><+ b(~miu~/E\휰6GnՎ0QrP,FuF*WUImqp4C* SC"A}b:r)bLI~tB?+?y{5xh &)=ڦ}b4zOBh :jA =y׸7LVt[yC7o0Stt< :0PdB;+ 묾QE]9l! !nS+HsE|_rPZ:ڧuhCK5&o_cFƳH~ T8rfYi1^X㩏cl[ hQ/sa?D5k05߇*# V^&H.s]٧\$DRX Ь6wo|?6S孿'mq 7P~76 VB9)QǶLq-' Zv9cRUw|G6t@[vyӴ-w?d Qf< @}o4ۿ?|qz#QmjrD6mڮ}.vKHiAڝh >7o-ѱ]oܽ{a *kV/-Hmm*m}Z?ħ}qk}V,{n d ;\Hzn~ 0SQXc__x;Ͼ` ̄j^8NO(#pZPG q_899y/:яmZ*VBsz?a"Ҿ(B3εmNqW>WrnV\pPe œ?`dܧU(yWXF]޺ugc]83NNN6/o_.&᯾?΂ 5V!Q+-A"R\r?%b ~)-O>إuk}59K <ܧhѾxgVF.ȯHPb ֱ6km-p~b?AlAgų0 o_.d{pUnlq4@M+c}K= xJm f ۼAخIVm6 ƞxrk$'זMQPF%,zfU2L2`8x}kv %rm_ JۖK/]ovGG]8 ?_R X,IU,0cO7P" ׶şt[0mkaNpXq\)v|nD3SQP{Liak)z+ ̌3gw"BT<.'w3>{ IDATǕv`3{]qcog)2_x wOXE4=^ԊҾ2RRZ_\yl{v3;}*Hjrقl3vgk>-&!N@\/êsÇh wtz @^899ZYp8oMo.vu֣OFטۦmr"8ݗ}@K`hu smSYi*+K8J SO` x;1<^M:{2бo~_2óðHP, }hrSZ/-*RsPdeK-v@Rυ qq@p"@m?Lx S $03}?o/d@.x[T>O6m.0gJ)?pxx`vtL @6-ө4n2d.w.}=CGεg÷^"Y_oCAk5q[*瘏Z賢Q+*6>ϱ T'V:8?Pb_ph ` 'h k%}gf}d]CbmVV$ ]kV*'1]ZVYV:8/CsB1Ĝ )zs=7B ׀Ey6)!`%N5okחP ReEjX3%oFv\z&~0)eX gcab.Ьӯ-}|m\$ [/>eOWTVVe;?{[%<6QΜr-tukGIǪvS6>kf+WBr?Z}=6SgkxY!9⌀)a-+yn)(}gꦎK8fy "ܦOyN"TZ}g\$z!"si}uy 5\05| ՚](I‘}|k7igvju]Įvwwy>{`vzz\AG+4rh\ynQ?HؔT(y1>[Z'X5ΘV{ݷnV$i_8Z>sTRV$vϜ r9Ns@e"Xo$@>o3e5ٱ`η]_\ri9z^jaߢςq78zۅP߷8L,Fo8>ʹ uqd?: !g1+=bU"C vԻx/u_n踔 @0#޺X[Ȫr^ j ۔I| A}o6P=8 C~?CV(ףqo[Ȳկ ~x7u;j lIP;d!Gx)7o,:.= XofaX`\ߖ_+oA Uq]++/޻1o=^D,kp./>=j$s,% 򲵮z+i;5`B𚍔g/rxs7JA$qtոKt/B;_K5Qe)hKq^/wp5mj41`{+Lidc!M@" oo, zvC)]Aˆ!>Tc T!V~VT!q\,~MsB:. ?XR̓%--";GW5 ^sm[.9QٿJ6]7bZ*, mFx,mu{sc.ߞZBA~<ι/a3\n}?3>uPK}PdՑU'@ϋk^?~__18]F+f-ǕUÄb;1|Xz/9:<.K_QJ[W\y cnQf|; h ۧ u9jP^-2sr9gœ}(V-,Rb1T|ʟ <2nϻqI ښEj3;oݺxˉ? ؒNk* %=Wː-?}y`p яkE*4 %TI@s??Dۛ]ȟHƆX1ASyJAO_*X@⨏eWg G<3?O+p qtt{w)tHʋG+2bAj8w^>؋.5R?g_xs U=lBm{ |uy^+j-ZQ2lleԾP *~" !3KGwJP{g@+&+똃us+Kd8>>~ihrhoe}.h5?SB5|۠Vh{[rZ[׭Լ0. R1򽵲P+qJ󛕐Bn^L}D;h¥P\نuSx}Vqş@ǥAg.q`Ng N³bw(/'!jy䃃je*:g-T~?^+%mU^3ϔ̈*Bm۩ ـqe#XEЇ(-Wh,DJ[z0+3Zt< %7R~:ߩLѲ.5S "2Vcjׇ }Yy sLT-˚ۗ.VUk3:4,v9Pmq]l5e0鳯4~~eͯE91?X `fΝ;pI+c\cn,LgF `TqP!WSsnyn}G]yep{2 5e&[mky2~P^eO}8jg߉ Asfu_FCSz,zLs:J@n: ߛ8 kFk (:MYzʝӷ[XfMD[֧m;Ϩm;`}ϮHim} Im0œ‡ڨ[ʔ&ς'}lp{Y㈆>scV^is-+3~wflw7dWCE1WekܹU(v8x?߆碽h? ^zL;.0ڵkO/X.H}~xUZs)8}Q D!Sه>S*6u#(l- w<^ɏ=?,5($Sȗo znm Nԭ ׆4zgT'9v3VDM;4 I9?&+V XVC2c's俣q\ի=)۷ߵX ?aX,B|7sAFPlP=]l` @_ȂtnR}L@lcEm}F,QfBn98~;Kw^+֭[YqDžDg2 Cc. 7/VEg:sN YN ^z]\ZB# ,L+EBKrn]eqԽ* (;5 &dG?,T<ڸP8Lx0[ݭJ:3eBx z8H)<ʸE[p vlw16fwm-¡+Oo(_ <AanWryV-@}uWObz>0 wpdˊxp| qy?DkMS@8/8]vl}͠qOXp迏M&^T:/7cq4Iy2ɥJAR|4D e ~+<3|:/}¢+O`Xs2(>ZFt~Pޅ tL@gm?&G5(Uee#. :v׷H <&QgǷp$gm EbLg XK3<-de᯵;Q1EYO~照:&t)ѝ콥`_#4y!YX(`:X/TH-,x2=Q-60[[2O,z}4~v0$—s ,}cy׃f/D<G0`rR ϴ7Ϙ% k]g-,+_`2flso߾}ít\-kn +I5BW Fjjه7ożՖZf+hYP l+JO07`G94PLy ~zfX(F _׍ jO:lϮ8sy*.Nkc>@s,Ц͟}ADžGW.>5 EQ t<3t-!^ i]u<L:O' R8b$d: t{'/[ˬT){ \ | C8Jsz%`5_ Wofsn3;Nq:xk,oɍØ"?k .> ́?hG)yF)_( s C(zA|4FYc#Xd|6ۮ];s:.z" ۷o0 _uҟLpDXZЪ@bǜ=>.[v1|~|1[I`AKcV'Q6ŚsкSJj)[!dl=3IuVr}p @(e+sm?ư 7s5P Hf<HPïG+{(R]n\[@g.0a/\. ^-{8Ta`vh^2|.Ѯ" YS#hą? ȃ) \o#ctwܓf5Qڞ% R|x-A4=?q%i@8kwAspZՑ/uuU51 Furmd·q+p ʘ[T}.tyގajKVXPSTyvxq3oFDžDW.(V7bI@뢲}RK`PmSԏ9@e}6A`\VU6f U$.8Bm#l km,tHgZQߘK]`EY05ԿB[tB8՚7v'i[Hc x;q؃h/'gn ჃL sB{໠"pO.V@VFkF ekσ|w}Z`x嗟;8X~  Y,^5P0oJRW |_sU+1 }#jݶ1k2Z 8KYf)#5o(cf!Lr l IHr;~+=5"vSX䚝Lv݌lvd?ۆ-QC̫2u$Z6PϿ)8_ G<Ǚtz/[__͖RtԩP[6{~ okGP *3cZ^_e !,N?z^ǑuOLD>8k i[nxW@)ouRBⷢ1pYw*D<ĸ؍9 "~sm&)~=t\(tᥗ^~o<88eZdVDdhV朏Pr?;dEa*g翍 -|_uo_&ƹY&>AVv{0cR8Bc4AE^"cp1e17Ni =@]NҹȂ:Bٿʓes{N1 SMozq k׮}uֿ=mFC|p[{3:/t]_r65-aev5l K0[C:٪Ï͇V> t?q=8[V<'[żgߟ88"vYV<7,lf$yP o6'QIyl골q gφ*IGG| ?&' 2YbV/hO71 B[ŸhIdE)FV kOvhZWGf@̂7:NR8{GߟOctd uXVa\9Zڪ /y`F%[%!UaŞ̬03Rd2܌g˿]0+)X.iwb&*E>7Q@ H-9bnP)ٲ@k w 򂖭&Of=o5dSǖbPJ#]Ms;A3)0) n>Z Ɋv|HL x]x޼FSEq6 $τaA3 mo'Xd֓qЃ9NN^/ʕ5Wzߵ,ਣEkr"hZ=gZk[V}fy=fʟՂ]f7o e=yNѧȇ kZLCr824?KF`YpRFQ $y~1z /sjH)k?,֠!F]JPj}d{P\T.fJ3uu8+,!Y[ykU[S溳r~c~fÒehT&at}ηTg p:_3:֕eH6;rvC>0p[=,3I8%׮g)b89:pqttif +W k)sVvQC,ms\Ӳgj{=Ո".pAK*gabC/HNON?ƍ7\38'fx&ff+*R)~(ι#dyx`I-, 1 9%`b-u}I_OVCPк5WcnMz;BWڅ 9gwOV-E8* Y|6>*?/#sï4[J)I1Yvxʝ/i ڟAd 8/0k _ucnl5w6EJa o6 L}~o#ɽ =Z8b!8pPq3~~h?׷5RK(X"ϸ>_?͂=[WJV$Rl1JIs~^Z L`fo{#:%pna߰^'.`U>uJrZV8[ wQR2-^BHEՇgy -n?/,y7*L@ Ǣ9ܷN3SA\}e_R>pcfL"Kbf}`)*{9FǕ)nfOY%Èg#7;+~s88>~VÏ/ ,LOZ./mM?>*6Ac.lXvwQ\hsѐiU%To3?gE9IV(+ `V2''ř u5=K眃]Ql 0ku<0`"'5.Y U#gfgMr8u??ޘ̎'CVH4qfJ, ?V{.t2߫6ǚPe [Huyg9 B+S_vZnqmj &N\e|Y4H'Qq_ >|Pf,ÿLy#PP.?1_+>yWYc90ͯI`V8"KwJzfCq ς=~::נܡ+CJ"GeR󽺠͕wfϋ2k!=uT(5Eddԟ:n(#,Y[$sT )|%ұe#x}6TPc{V=?Oc_:o%,؍--Ts ^àԯCq?}1[?=s3&3{A}vzMXF%}}S[i0}ʬu5WU'W}-?Yљ[Ye!WgSƠ0MLغwLwdzgEжE}L˳⨴~$.f(p) a}=zPs_Jk\UW3o/DjME򨒣y~~ 6}; p`fYdcZvTT(hQh췏EEa-Vk̐,$FMk@˟ Vd짅l1ɊS-Ơy+! rl>a.cgA[ƹǖhWcuy4yl׋8HQ3o1gXJCc $cҵ#pb󥬐Ɨ.>ozjlE*٦kxע0;_AfEV>mG] [{>]IsoleżX 2q luPz[.˃^W\wP9~o)Uf2)@98h"98VṃJs]3ٝ`e[,}֭8G}KgKæ}Ѡ9oɚkk̴B=׾FD.ʂVJfE f-ޑߎV 6;|gQ#f ⑔uSUn({VXkAg<x\{ީq5)HTو3#4Pi52+;:7pNPJjX,"xV|f L"{k+'9'tcdXz Fˮ?/ڂ~۵.kTh Glr_@Jqa[O w}aYbLXҼoA(DR4t6B85amcntZCy.<꟏ @/dz?YVJC(ю{ym5u1&yǪRf]L_($&hy_ @Mxj&]D hX8Svx `Lp(g׼'?qWV-]yFTМW­7 HҌlRXkW1,7Tx }%( 9gwsg_Z7vksԺ3H`xYQj޻MxjYTmgO랮=ʭ;ޣ{%`$ , 9og(Q@gm4@r \c1Hs& 5f4VQO+3=y>.MzԮ I r4rJBÍ..dE"7:Еc!x ۷}AeB% <5 ]f"^oW].%Bmɵ\}ՖRH\FZn#,?N{\s^X)ɀ z4Q7yYD3ؚHģi ߛz?P[jP`"-wy\z\W@) 62A_q'?sv:9QJ-fsՌ9]kasl2k:e)R9`X,38(AyZ]x2>!q \z5 %_[mq-!4hquH>ʴK$> +6sA IDAT5:vf,8H0w>,|v$ B;`UL N?j* tE_!lr͌SO}F"a 'i}̢x3ZL>ٰVֈ1RP̅2;ve׼9_Ê8nu >L+<(1~]aPnv#7'jekF_ d$ί|JB /"e1e eEY9aVmnYYЅQm*>X}Լ5=: n@ FٓqZԙ1ԬAVg]zF}9#PX7clR`Rg$;N"C| w۬PyVAALc7D>F83:F Cv^-Ef4>G&T(=:PJRPPhU/ fD,NQvX3uF{12v˽ U;izϹۑN:k*Ӣm]z޷չ|_Vf3s/-bwLK V>t|4f!+Y5#P >leD@t?ǻ\[0EyYg9ֆ=A폲}m 3X/qse،}i5]x8>>~3_Mn 9؉be3W]ǕR.Z?2ۊDlz_hykܚ4ߜ#,"~56Hĩpq ".q9u/{HG z&@lxNs@NSy؊`mT_dB=zA?UpA KSyFf[4ng7rL@\O~o㑣+O0d>1UCnUҏvne_«}ڜى`WD Tp:^(UkX&jOQ+bL_Nj{>k&QjͶ-Y@x_! D%DVbG@=Xg! aD>o KgV8q-pa)/퓘ckrʢ{eI?C!.4~ q2/>z b~_ǎ<8\z ȊJ,}$d V iBDۢlGmik[ Q4M[iQOuyje׹`rwX:Uq(,pA{fH ?&[@d7['Kk1'@Z{=pԿʈy_=G{9;!.]Y1ͱ01yшP}Պ,wV틥b)[clA+ॻr#k8 `mQ.Jf߯]X% hWil-{DUN,Sg i::U\uS~޾q7+,7 ym lL)aEr@]YEu+'~u{Yl*<>O[%;' Re- Q]R4RPl-Z_˿M3W{Q|?Y;#qƍll|C9P [hQS5jZKo㲼jԯy1TV" ^uڕW l۱-DVϷe$y{YS#]#OQZəʎmpCb厅~mj-Ys#'R̃`$/ln>|js'ߨ/xY=,쉱B oym㵊܊מy㱢+8:X Iv UZ @:,@W$P0{4G@n傴\url %u CX8e±H{lJQ/ 윳Vj㪊nmZ|g߮y?NA(W4NڦHDYb~2aTD 9RǧfJWU>1%vx Sp@m5k-9 =Z=:A zg8{܅^esؚǑ}=,ط|=?-^˱}_YclKFȩysY @s/o]fe%rE72c/ͅog}+{3S߫-oS[PAex9˪Ye2KQ s xsIɄ>6ږM*yv,vCƶнsٗ{Vp=`Ľל4CSW:}4e6#TC0P yQ |:O fNcj3 DR VɵS:IPw5w3 $j1Uɋ}IlE9n2;y,b 2P0n:b7/0QFBI3ϯf~8ŸOㅖῠK1f+OcxdxjbJJ_wp_@V/Ga=!@9s;?0[AR|@ ]wDX;VB̌J.D0+xcV+ϊ~uk*˙|=|!:x,/UYRg;o@|}M+R6+Yzizpe6ǪTzHQ >+6q_FzgWBPfk1LAے+ g 7ˋ)+ HZs}=zK>\^ݞBl\V.Vlh#-ޅw2wU=kVd0[KZs t|}ҫ5 b <2G>y)L)E4i9='!>67O\"ƴ8)csmfa?CUfV &fe WCy@E#nH}ӜX3@Ns &{ov<|t1gU]uJe9Ԋ[q}R _-cv(]^('@K3mr"&|P^.o!%Ʌc:?΂D(a " Yyyu_=SjwE?&F_4_sʫ} wp@ gv51y[&U-ӼX:,J9iޟ=g]5=2yZN:} t3B$BtXQ,cj3[35*VatvqA AbqrS$1Nӽ>u]|!:VͼwU}xF?} 15LK@֖*(YbQ Э֝n(Ug7Dѫ?(8U̬M1@Ah T45RPfh ahFm'j9T pXnnEg@ڔz?E}'ĴiBZN/w~jct9ꟾfZіZl\c` af>3 sns:*T.{\BwQD+?]I)+*Ž0_{ 7E8T[9Dʬo>\"(P>㢐h2V{?3i Γ[9~!۴.X_}L ~֢C 9ւ(Z:{V!8w`-:}F0֮P*{=؟BhC(CEtY,Q0u·4YϚqlϙWQQ k[ϵf]}nQ˟V 瘖X+z aIuMhYXBð*q7e 2{Ae*1.4B^Y?&4L+S],`kqj2zKAN4(00 M@YRYl0A K݅/3bj'`Jp*0X廎uýtSŤH0rn) ~XӞgk2Lf a ĥ@T-X{֫&2(5v8U1p"-T!ӘOҴ4F|oi}m#ok͹uAmnn0:~];C4 9)PsO"1֣sG#JS2dTy87~W 8[t/3\WS*ֿ^]G*(xUC=T 7Ry2's,,lzgM*"x[yWpx1P-{ƆA3ZhW2BVv{ #ϥaְΡ{r \1kVч+i'BZ.C0Q{}DU]Z!:z{(B U(H=83UO FR)rystKh{lMμ[X0cĪ{Rugu.y&K)n&fӬ\b:s̵VLsK:[;UO ](TsGx``+WD>.ũLIə*ń%bbhwGnhCꊠ߳ A' KG`HGoc|͢mL6\YcGQ+4gZz۲ŞrXS)z Ms%K$ԽH+Ux)6s3OҬ\bݽvMiߵ82EkZMAZ~[Zoh2S!Lu "-tUg*0~g;EVwz:[":BB_!ujZc_'߸-+xf0|#b_J·J]0yj7/QI*>tȣ~?1{B`_[;U*ʶM߷S* 5 )Ci)@ ,DL݂s缥NQиߣd!A~]AS mUpQ튪˽]ɃQ_pH mxpfiT GI!(*S(P[(J)Pj2KuyV^+>u*#\$ s ?{*/¡ UǜjUֽ({{Ee44mpBVRF(Xsc}b\mO*v=)yu95NUq!~MtR ,w-[ i4+.ՈUu,^30n)ZYf9J߭)W1`ִ>uX0,VU<2rg& 'W< ɒB-ʄ]ɵuzZРCE]/[XнEXc*V\i- y1!U  Wwh~Nt75cٯK"?)|ڱ0%Ytɗ%#tXiaK2P~԰ڨk<LsL+ bV&ϛ͌)AsH@3.mjНZWjXYrw4hWPeP!#HE\,믖b%egҐ76ָ ]MԋLdwӹsJ3uY亗F庒cf4KG7C;6ᒕL҄^ؙܙ6Dpi.%#ʁ2;4$  A!W^CRHX['öH ܙ2իu3ԛZ~C P;}rW=x3ET;CQJuLcZP*q\3f(3 7aKBpzMZ<<jTcjzhxH,i WX[CzLGD}LTWͣLDbfVGkL\q>GB0kWT*T2j'V fA. ҽ`(TjGP="x^Sk^!v߻ `Bמ%iQLZH:.+1 B/:ݫs*x?ZsaKDp(.7K#3 c;jWXPvEH9֭+Qf+CO[ǢnJ fX ǯ-T\z I+i|FTA.O vYbPBlfP-Q|믥sc]tUy~-DĤb֫+Yx\ic{0ޞu@>5`m RWi|[m*E֬Lr>MZx5oʹ͌`F. %'N|C)ْ)pP]1}Pؔr1ɜhy)y>2BBRjUH!)ql+3iZL SyYٯ_z nxp%h^$7_let(q}u9ٟ>993N_cyOHWFK>.ȽLYiב<yd!G2:Ax P[H.ETa("t3Vi?:1YTܿ_Lh=pڪO~Z]A.hK 1Wŋgl2Z}ގ74mNB!g JXY CPAs Y"_][nukG Q}pCytq/.꺿 goL,`!g9К?U)bQYyb1. )[r1 Sd@lS}Hm.QTXK ֈ :MReb@ZNVV`IŬF!7Ja`EP Dj̸9F+фEJG_cԢ _-qEk}qb̴pY䅱uBA\hr'-a(!J ] T4M#=DSdUPSؿJo<\bI{C(wqh0*SRYݗKvweh@lƿM|f8 %|K(GS֜Ԧ"Qedi36]* _ u9a{ )χZ?bD]%>S>ZN6dA_d/pրY`E˞H^Znm!a0dјw3J!gΘD<] tO)eX9Ui=WYOn^LTpP`(m($k#Ei{e3W`5N" {>vܮ:I GD4`ҹլpH(&}5Bgu]E 40PAs{nkQ%OGGU?JfS9RT֞klaҬlNz𹥔ϐS{X`#aMA߽M2c{`+B S=u$CeLX!a 92웅0`HQ~fT!Kas͈o\;^@qZޭtLה-.ҧ<}^8j]B5̋6Lۉv7,p~Qgy[ PY̞ȱ9λ[NXEG(|rƁDcxhwfz4+_|X7ZYڰ~/FXU"0R0A&SS0e~|JR8Zz9 R,3%f7g(Fw zhF*JFPi53!_-txLUfȌ^ h0~_GuM=}R6׹mHVOߵZ' Xl*D'qvh?1? 8L{ug+55,kU9~W$&> "Jt-8*fgz\4+ǰU]8L*}Uh*H@f9ia2UX5pB8 HE/u"ESx:%R(.npNUsƤTTf@=׆W(p`ks>{l,kED/n-SMCYQp v샧>6:$.[юH"W!Ykv^M ?67J)T&u>yf`ol-0VF{}:#Y0RB$lg_c>d>9-<>cj>o+ z1Z|pKCqtV|I+/t9ZNG& R>uU:)P)8ZX=C8G0a`g{{$jlhtn_Ta@DJ'h@(űʪD0`m]{b!hk52;ڞa;9kbDq҇|XH!PRk)kȽ'0u죍J5;6`ʠ] T߹0Ɯ!V&®ͺ(8tAS11jqQaM1q0JWD<>s Fv{1+ԂIń#)3]2Z. bO~pժ»1 {JJJCc|Ѵ[Z-],5WX(],&}oSOc] .U؀Ţ iZ&ZR¡6pkwa7Z6!죟aii4*H1x7B4F Rc;U!*Cbl ]派YFJJ#T| rub~:UH Bʃ(P{ ^/Q+,9W91?_]*@Z+ ?ݛ9>$#*jGiW=boC~cb@ϖ|N{vk{ x 2&AjXXe媎?n2@cT赨 DЎGjsXZ@)~ϼG~IS& 0 rկ?bg;*(bo/.VqN/Ev;\{LY Upw۠e?,U_éBZ0{.w'͕X'_2ȫ 8(d@1 sIQV1x^]*y8D:$ee4$jt#:{ r&*E~x495/D9=~ZT\;ޡY:~U <:\?SEJ@cpi{1ǞL;9sJPBA~.u]8 k(MF)YGgP-{v 3=vQ?-!1Geğ* [-AHnI^sȴP58sl.A9LuIȐAёb~ J%sf5]xoo:ç^'2 '/KWJyq$"[=mIIwժ@`~zMҠ&\ ,c?wh"%)>gFX~U)eѵZ"#Tt 9aRBt/Etg@JGvڝZj%e.z #*!{JAZG]QQZ*0t.t.]@gt~c}:IPVax1J s@0xH؝§MԔi~btnU?ιRW_ً!vY&]J1[[TRls`x3zT߈bN rEȈkmȳUAZEsݕ@ּB>,Ԣ߬!rHLrKlȨUi¬HBr _X+O}12 Bס)-Q߷wRP6!b*j9{*ֶ 0͑6ʉg:Ti4UjR=%?o1f3^L½Z DFr<,khJ܋(k]1̊hdr}iHŌlf`4a^]ݮCޞW贘nlesqнFnB4ж=v! aPwP2؆WCF0ܬV&__2pX0 4?($;Z ȣBEƫ[;ǴY*}5"EP+):~EJЍg$ws95kS5u;¦AG{+s*/)^#ƙ31Q=Jcȿg"h iV6Hg"hqg@e"VK}>iZ-ܻ.&!_n> *{ C DPQjͫ AWbkP>H"5U{|``a_%f}S?j5Sן֦ LyOhiu<^ 3QE"j g8~Vr#9" }DiORVU]o$r i?0:EZ(}dVSkϓJ)lD9\㐗\W[R`Q~V,6ѻ(|pͤE{77Uhǯ-LGP[^S@6>nH1#wK0cJny*vdzbtY-?T{tU?! vrp?]|_Oq5k,=o1*zekХoԥB|t$# oZ}LjyǒM鵘hV48y> A:~ bmϿcHӾCV"5mPSrcj{+nɓ'LY\ (I@(3 ,7}C1dH];ޥ\k510h~wy|Ze9 Iv4wgߣUZ oNHOȖ?{GbU}@ϖukVWUԁˆ*Uik{ʔg=rJ`P:5imIq>-us8]bĚXPw.JfB/67gW4#9xσ,pTٛ1`ooZ̴ 8MJJa2?*쳕wRJN0Abdcp ӫܧMadlc(M>LB;zrLz(ȸFAkL+l &:4iF[}@:LlKZdm0#`K^bmuF`@("sZgY"=jEsTjFi._N+3'5" YԷD mb5xT }8Lxt_:ʀ [Uru.*T2_\ AWEipyNߝԒ7ІhV6@ [0Ί g6ٮ \гL/_TR-Ąk 727Bh!k ]/>槅1)5.T9h1ӝb$ K/jjI =Vcn2ͧ͡ fhz" !hW}ѿjz0z^ڻT1R,Pk2~G Jr Z`E4 @;>tXV; bj*`^gG iV6@0%Aڎ J" 4tkB@0Y59rXQ6!̾ue5kD1 % fUm~x\ߏLʭWUпZ1"wwK?c.={X<4~m¤$ 6Dc gE(A^5P~>ԯcQ _c>U90+dh;\[@]ؾE= EZ-  9mhf`TJ9ti~WX" ֿU/ `)}Tk(yX*ZQ7@ũpfNg*&Pf1yhmT[7~UgTiBQ׾RǍӑ'?WuoﮤN^aΞRy*~Eշs6+O^,h*>.{4su׹(Ż P:JME zf`݁A0_fyg~_a ch0~0z0ݬ=R5~ZkjBȨ.0 A 5bU( =5B }9O>җ'OGbkkqs_ e!tYNd^@H+z˾(\Uַ{J8简rLܫh$++Yp 邠*pm(,᫤](T\,?J  ٿ*u𹘻H]A)ާ-i=2+jo<7 h{Gxngz-8Ի41V)1޶6A`y[09e7AFQ>c;i&KX-݀7U4-ZiyZB>%aW",ĉqwC0/ 9r7|3~~\K]b9M-q7wuW W___}F?!*x8'\^$xbM-tvqX^0ku4up+)1rtZ4PJvSWM瘲 "ͥ hoG8VΒU]Jz}w \ΗlcL T&2? !W~yELLZ & |J`(Dʰ5`-';n3utqw܁W׾_~XTH Z'pa<ӧO?~|t@!-Ƴ$L2UP,] M>BE]M3ʷW6 !xx=zHQyRծT΁TtUyҹk?@yι xu븘 Ьlp{J@kpP"`$+[;Av~TB%U 1r0i:j)[TJ~ F9 a~PBlju:'RŇ)"p,\;x~%p;Oߍ/_kN_J:|'O><:j6,صEnnrt$>UF9 QTRM5pԌ 4V$*y-> DV5u6t~9oZRDڞ*+T01OEiGlmm5G;Q&hc9խe\YCq묑U d۴x nPK[/ )3vl:.r"]c;wy )K?W8Ve~^EG97x[Ƭ{˿/l]keڗjN#~Fit>&coSzp\1t`a6CuWW:e@E}23g--+R: Y'se"@%:i[qĬlf`TJ=>v0"L9yy[B2nZ ;ʚ`VVgcZ(_T+ܢ-rFU ҍﱎ=p#|`:?V:qnv?XU)qPpԩK)=V R^'_-D>0P e4].3Ƚ  ZJsLhT j{dsuM/u< ϾVjBz v7Ao}_9p4+q\Ff8H^5t~T4Zxd_R]WnA Ci:LWhA lkZTV()p%"2Eݺzox/ }K_ŸOU7\ N>="F+ _e@}^`uCdX]Q!)2ÔG<^GjbZ3 ap mDq\Bn2Q@zsݙZ04*k֌lf`TJYnBe*{P=LIPg-F+Y2mЧW@ScP_?+ӕ) r!w (lڭ~N`W%੔%x߮"_ OiU茗_`2^teĬ1‹1T(Z_;C.iiFKmuJCZP>]!=8~G oCW|}MϜ9;x̑P}.8u-=Ok!]lVsvB #9ULJK| d撹^!ߣGrBGt-2ߺߨLQ%AXawԊ޷ghRwV 69SR&S(E$zp)-j퓊)ĪrlwԪ.58O?gZ,9f`4+ZJuh"hgX؞ jnYC>jRxǯYwk۬/{*kOI)P8#Y6|d{c0EA8bBJEhW-fR\Ѡ $UbTb6hbR2(lW&̼k&D=Bn _zloQ"26tIKy54,+_/RD:7n пKY!PH!{N{Մ[nErɔiڍѴSL8?]ZcXUT_}c&J|R||Lh ?՗h}/+ߚp4eʠ,vA g9Fu@ӌ&O$s~]XUϫVcm(l@vU߀*)dqxd af-Z~h\NGXaC!=gMkcW[$T }?č P{mXľFN:Q%vе}eE|^*n>;8[[8 (b)n,=bMo5Xs ˳BNO$+~w6ߩ@ BnoȨ=رSZhX}Ţ Hǵ>v}7}SST X@4]Çpi|_?'O̙CqAwG(KuPGp4+Z c ˃#;B~}i@O;;bAcUX>GZ&Т7 :xd#xRp 7/ ߍCYWZ͓ӺU jj@YIUzݐe]"_;10FG[ުx\!eULWiTJi D݋DR]3@)A{unM?h!RwQ9۟3}}46C%gΜw|!;YFb>nqw`LbtyPV mniVK3UhP#s,ZRL^HvCk99#yLBGu│߾}o5A|1g^ԭ?ws?w}_}TEً(* 0GdXjiKh5ֿ*`g#+vNR+-VHGK n8kb[y.dz)_5TBjǻڬ /g2ALQȁz9B?T5Z*(d~G[oLV8x̙Xܹsx{ރ58wkQcA rMA4^ zcMAW| ݝV&-Eۨ 9}xm<~ڗ0sṩvkUVwq1+YcUKaX,p9 7|B{:ɑZ!^3hM_?/K*<h_{Ga2ym IDATױTЅLqnTZ[[[ƪR.h }_//oV4V bݲnQ5[x`JVUw*^0VHsVHs,U,R+dn(sgI=_/#u類K>Q|^r\х7 F" @vt6;q, j ]n5Bh X8s\[a);y^`JB#r[9Q!TUJtQsǻ.\;^{?ޫ 8>KgϞnVoma^;>~r 몝V I:?֘\ 14鋘Βb?jd߲ty*6y'xC| ]YXӺBi&;YR @~nt谊%e:&g1=]/dz0ZS}EE؆B`fLL- ]U; a;y=:PhRcگn8[ M`AMSqJາDK/eq(c5¿o߷/O?8t萬*iԴtKA'O~j#VJ%iB^|( t~Jºfګg 0podTdEF=Utaeս}\ [n4uB{c3 0*Iթ6U1M (8>+oyy%3IuNu7(38B x)::wdL*D}ِ)A ZMb|͜wG/7|3^oőL©:|x uW_}5gnu] 4|lGvCt^+> Z;`?&֖Z򭲡2*?kV%ob>LLx詘=~VѰYX~RN5#F%dy0ۢ$.($2”Wzs)ԨVDHY-S䦶5hpjfK9eϱ 2 YךVK!=FH T\*֏ko?zEwW_)L^Z2g?x5>я=ϸk 7܀>(xzԧp'?ɋ53 vx/ jL+eXp?wV*QKuƨ0#=ZB]>X2Q,mo]U^@7vĂ}sx[ނ6n%%-=Ev?t%/__x*'= r n|Cܹs7|뮻eT!We.񡹩?S~Reŀ g)J{Alŭ~ kyB57t'CO(`؞.5fBA)ѯg41#Z,'SeZ }}:Yϊ#/Pk]1EwhtC ߺ _:1Ul~PFG+T Qܒ~lIƸuǏ?_?óYo|U笇 26pw"xqm?wAg?Y0p&kgeYL6`~tAO~ȁ;Bj qXD;AVm߲^gJ? ~>;ND+SZϭ$ kM/V$s"]91kҬlΟ?w܃~WN;k@fp!Q&~v[Ӊ؆ό+w O880By4Fa.])L] r2(Z@taVxӛބxYL??WpP!e\ցQ?sjNJooƷ}۷]VE|D۱wtO6"r^|>VteD`Q4*x.}jOoi&V&o9(BAC(s7?y_G~yҬlz|w7[t@-޽va8#ό Y*~!u]mɬ8Onǹj%⳰b~'#j($EcsC^=\hK_+"_rbn0 ~1kg`4+㻻{ۛ>Eb iL9y>3l|i9WԽP &edfU,jlAL{ ʮ O~k?y3L am{4w ҊGbg7>}zHjA΅n,%R}= ssRa"GB-_QBڣ}&Q!΁-]8Xͧ+ 8ǵ:]?> hV6@03gN6~0S öLLQF=J\X8d=WG V~fxkMPc^xr)⼍St_ ULXG*/ytQ ЕW^y+gsh#0kP{T bݯ,kZTMt7LǮws fLC-u8 ~dCort~- TViPF`R$YP%`|@- LO89s&<Ѕ ;aNv터עR9]!k|tyydO#ww}8{,.FW]uZ˜nQT(/BY {.ZIkQ0S̙ٶˠ:1E46~(mb] goikbmߑhKQ {tMYk/:{2ޞœlo` P3OP[mdn>`uQ'Uc*U@krv$-kBH@[ZOMS&Fe8e[V| _iy{e1`|eRH(ֵhl]G??⮻zVUؗ)ԫ"){ w"@j{~WwDz)}TxwS.|ziLʻX+H~)e\3Z[#@7u1*Ҹw هSN hV6@OzғNX\~ƘvvvӱP,ZŃ>w?oGPvfBSQ0!վLl0jBHJ:-WJ={U|T4h,ҏ؏[n9Bgma\@ExWk_jϫ…IwG3ϱ=\$OO?>3RP|_mV& DaawK4ھU("=T|0pik*SgbP欘~=s<O~/Ьl*K)8|prlo/+.=鮻6s=|Ac5vCob-qe<2I=:wc ʊ*2+WS4ه wy'w6${x \JC` %C[S4ſ9 ǏѤoOЀ·孏Q)7tjsW_ ]k+cCe0{Uq O) x(Gk4ι]r`(Vb1+LP<4cY兵Vlo)OW]ut֖Y/at1_",_4],Ɯ :V%Ÿ[C>YEpUjuWZFGA#!25#5=3sMoF?__.F~wuX}nO? 8t nYx3C+p8{,N<|+ԧ>}k_W^׬ * k4*otQ6FgK]",$GSѴxgw-+ lɡ!Sǵϋ]UqaQKGVUb!`E24/uw*?,8p _m1ѬlWu۷oG&t|(-|`5f 5t};BI]*lLFsRWVhχZ( ѭ"91 Vx߁'N\|lT]1ڹQĉ?ӱcGb8wu!h]Q`ỳ :ȹ"+ơ8T9fFW˹/ӕ1jDFD&+g&feFzRsJGXg`C4lW[pe𲗽lCkDZgk$?8 ;3Kq&躠9b,G+RՀ1bł$y%1 eOTg_<֧!S`h*Y)Dmj(߫|.08"xY1g梢Ry( NJA<8GS4"+-1o|ok, <#)khP hJhJ-_=NZ YIKr'O|/oޞ^-ߏ:OQ !jj w"!N%o۱+UI]Q)Pr?o]aYP<1*u{鳞o? Ls릑ta8[r`br{HWEʫ|=\cy6"?8kD8*ԇPƖZ1=ѷb߾|D.3!]a8X,V*<;;;xK_.[jN:bo |ۇ11eٟ nfx;r;VNt7?uc+8L" CX8j]4^ Ouo/r4/cua8nzKPG3r9q3vy٧\&>U7xU*?JAlNC$?*ˮ 9[|K^bE/_l1)J}9NIT>SrTVhE>(~Me UW朝D!UoɡC3?x n}В /֠s r>}{Eu|,^A?MkthhV9a\EsChLa-#"σsՕ{Ejn b1`Bխ-o׾6E(TW^$`jo~{}{{{ba ИL!9j _>i@u E=Òuў6Wk? Z Oqэ{{{߉__M75y@뷴|7? Z+ `cor9@%:?׶)q39Q%!ާb?”5qWEqW :) ֢|\%'peM?}wԯ݃6B3!:r:]J9->/<`m Q}8{iVaoZnz܀/_;;;x3cǎرcxӯV֏-1PM~-/v(e@H[jY}^^#HP.믿+|o|1󪫮­ފWUx򓯞ѯ[)s泜gU3<ƞY LXF(5 JF sm wHMC)Ϻ%确|à{|0"17rKo8,ΥWb߾~6#vwg᫯dwsuӬljWJsu{e]kITXrȭ%g XD xy}ݸQ}7 IDATkW\GeeL̖LRY@@As׼5xK_j{r>5ZC|_WYUؽ== >ZЪLy?(e!87H!gܹs8~אއv.X,v-Fn7HA*sF:Sm0a o>'N4ɓ+}jb'̸֊W]u]X9rS ӧƱoLVYl3j vKJ 1g a؟g9y_62X5%Q}s5iO{ZDa'{(" ZB *Ҹ^-పJHJ~ ):^]gD(") @PrU?DF+ =\g?Lf`To*$>e7>jX%Ć-![B$p?# F N'vlAv:i1>[(d43@90\s)2Bj.={WrN VLl:HqE4%]/Z*בrΏXy!دA3N!UpxΜ9mo}[x@=3o߾J%τѶO}oN>m=cK̨iCrHY'Xu awj!R Ը?9XhK :<~3BsIg?02~]^|GRRSb}㞉_܊u?SY2GX$(׺ CY V+=U,9uC*'s1|t2.V^:fm%̂ `gmGgE0CzV=v-k(`9^_ y]N0o4V3ϹΉf?}%W5G+h^u5s2hFg;\Өm ,YqY*zR Bъ{C#GP6#)u@W^r=/ MxnT#G'}sscWm</HK:ҩSJ)=/U֊2tZ|er8vqvQ9HA-; n+/{ 090) cD@[{ڎλZHW+"vx V9CV`CL)zstXU S3 ݳ0ba֨D ĵ& -2PxO ]PY1=QL2y]2␊q9;(}Ng /˗lka{>2y8SNڰtj! >+Z*ןHH(%4^8Xǎˎ92UbM&y EbQ(UόA-kqq.cZ1XŐU1h[smhVNlK:b\*?H|eÂTLk@lJk0FX9spFԫBY|z+"7UE^L*koVb DՑz(S|yFV+, _+0&)%hPp&Ct!"ub%HHk(J gsѼ21X|7OWE #--d#tx]C'yU/CdM&y_\iJ[H@vӉ HK:R͵畕6I5|4.!p̙vq[_߰677~3 .O|Վ9 YhF-4P%Y2>+`)TH締ަ;Y~.sVѓ!P3\Q Ρ8iNh|mL\\,?|y聕c,FZiυm09断W!P$gG+qHRKzRΔRȐ~UN %?|+++}}7}}'>qt2 e{ŋy } 7侶 #piJ!Zfx`)|f cbr}⑱"ӲiŖ+`TmT[s).jqs DEX}Fv58ԊPC1֙_׆bBG"{FpEl4Z5DoVY\ퟻ$GՂkt`{"ߓRҎ/h\g39$8疠ym6e}++7ѾG>Q!|{+62A`~^Iv"!ik'=ƑNk(|JbČY i6@'ʜ?g'N'Ƕj9_s@:luE[QЧLy~>VFK9i^=0'UuZs;kW@zĈsFo(u _߃w0C0, ٺ.67<7qsr!;tPٳh߹mmmJvco4Mܶ%]5-L .朏:gV (2Q+&'ns:;{]|&!ŋv%*0o Ljp G_}{ow]wegΜ&g!UŒҨsֆOd{-ˊ@w YHԣgܑӧOۛ:_=垈-"1½\#32n8 OFTSz#> 6lBn'Qwf`(ütpKOmkk7l:PA^ѣ*~,Xmm23{ӏ]Kچ uCf b@hG{-@5A-$dsF|w[ٶ6ܹ.WnQg(CNֲ!wK;kkkvg_a~;h$kr(W@z$3dϿYе(K#שWCmET}9+70А"OwBR 6̩tp @i|=)]P̳(qsY9,}7zyM̈́c4ʉ >"OL&[~67'e9kt߾}v[[W<67*e9"x")=+tIWEKP~3٬/0OVGoV̚ o3hL/:hmm}۷Ҧә?\9lL&"=Uָw O 766cO~;7؏[*\4] 1S@NVʑ +=o5P׾iƪs5o2(3b$< An(1!C\I-}VBD;3뫾Y=ˈ?Og><+ "Ax1 u,gŀ9sp^?{[ˍt-%HKP9n IL&6*Hޅx<ӧةSm}}ݞ{00bW'm߾}m\PPV;."q:ٳg{GzgX2ƑE;2L:E@](J+tDw|~XZ:~gܦNQ!C!(Z/QbW9Ჷ)(9eboip)h?K﵈E-{()AlPeoccb9ȥYپ}ȑ: `NYٷʮ\#wb\KFRUvA :! ht:yvwOOW _BŬP˗^03ءCTϺ( *ZO)֖:unOVnkj`zQs{{.]dg}* W34΃XERGJo5S3Njх … #Zc;;HaI3:Y ^?#\wUMza^y'`ol]Bq:~sftc-5!Y"#M>W޳NrjC:ƿڏK" wrQI^G`?B{XlkXG+Ľo h0 9Q^hSwP*./<)u6SweeN>eOtf/_/FXXv%pႭꪍ)W-kr}cNJh umnnٓO>yUx+^a˿DM.߫%kDekwM- OP5"F.3bB|^1{-~F#j`aA\%Q8}Ɩmld~@*M ﷿+N^|E9#Y[[Cum8cYo[rK_糳(@iGڢffl?^Qkfߝ3!O&Z%2XnYD32ľz̶a?XϘ$ kv;x+`^3 . ~'iyP, |xLڔ]7*ߎVVVle9rlnommٕ+/3]x=Z МL&6Nmcck{NK1Px򅐃BVͰw6hN߁XfWEJx?2[Zʛ~L&\ YY;q9FQYxd4?/;^h[QI G!Y9 B| "C;)ɣG.OTn8w]1@=*]o )g h` ͖5Vˉw G7P# R3od xGCٳ>[`TO-FlP.677ˠ )HMlha8jmmIFtuɾGJ![]]"jp?'KCJ3vqɌ$T6W+U&KkkTc{"QͥRtĉ/^p9^i6"ɯh0X[v:HKM+k1ԬL/T8eFx߿.'(̓~Wl<^رcC?j'+W4=v-ı{FVضUZ ;|nnM`d!8kl9K+6ȵ/C &r0|bN&"%J6  ŵ}bkklmmy msf<>81=WpCΨ(V,.$J1;rNm^v<2fƢxcffOkIו Ygkyf[g(#l>׺F~L/mnT4]J";L7'oy[-oy˼޶&v%;w=So|Þ|IWbO?t !CB /(i-7F<ܹs --Os|gfeX@x%9܃}R6@?0t*? v|bVJgluuuoX++cRf_JP_j;R\(9w=fAq@Y+6gOLVF|r:s۸`&ס҇_%]Z*7ѽ-G+`XeX8nAIh @ ,}: U 2ղfN'Ok_2LF,pC++k2hF#?j8"Z3sn a_JLB󣂻ↈ厡@5 LssБ(}TLk#xbNұJ}I`߿8`fVZ%9 $R9RcOk^H`$ł48LTORf)@<( [免}&ez+(8rY: wvckx_XPLTDxNA:KZPᒮ?=smW8e>3+/?8+Q*:>Ep#]ط[@#e$.9G%5z9FIR=@n46l-k=t.-`*vQ(,}F}e@TlZt9=׻ p=(Vt$ƭ>Eޯ8]рD)\Ő-}Uʁ{ uyjGeϋ h )fwL~)xM?qwZpŋͦRqW0 e~ - ۨG;{L:}n1͖7 Hౝa bMX :FԱ*N>EtaKIY T{eq[>n'$EPwB|c^uyϰBi!z_YP5AVSWJgs"rxR5̛QNEƀ%K{ձco%]OZ*7Ο fxT,L^"B' fY2A0_̱5E$Ck$|,@6rGD5+%n{!z^{i+J s𩕊.(.ݪˈ0FŘRuMRx⸱&S+^$q> )p/Q)8-ѯXK =~?j_MH)?5ׂ 3d0P70j1-Жuʤ#I"(0QGŤZLGtp)S4B$%\y\ ɷ!Jd/c}pi~zb ؐާ{m |Ζ= su{YPc]7AOKϯ9"s510X`-s:X[)\~Tn"Mǣ16Uf "̳'QL=#~["{ې~=u{A_F 2{w#]8it9OpkVDreEdIRYe]@K:"Ão\_RbMTUTX!4g]R.shJf~/\Wm&O)seM Rw'Qy1eVo݇h{e>\[XP%<[D+j D}}UF'ъcYDND7DD-b[:6 Y]S4w6DX3*ĂGy W>U6k滃,v)@q?p: 9[{ޅ{kI>)WDt- (k7' O-K+>%ÔU/z~@j[[FHBjݶ `թ&J՚`6ٹ U@@iظY-$F!Atb\#>Ykm 5Fv|W['ѽ;>'},^ B5߰}Rѻ\#$\d}6pt(dKв%|zBY KK&l 79S}rnDa؜QX+i< sw;+Ela7EB*,(㽵҉x ^Xlcj?ZD0|EEHtopkb5L- . XLƔ`~%171{r|0җ1D|ჲ?YŜdW(xk dqڶT o3/zp RJ%<Oj SE3bp5 -6k[#oh vp{k.|Z̭,Ώɺube|l>ζc|.ܢ5kP]зI;g&Lg-7@ gie~fИLAtz_=@0FE7c% E4]- 5Ho} R(\=_9Y[u#Ǐ tCi:Üm^0{`nFe"K o鳴l=u]W2,b `R\TĐe*u,]B:-31jabSøuW|pК} (euĢsi ZG@pkq*R[ f>G>}bJf`k/ ԗ;!bqbTY}@D%!+v-kݳo-[@}oWzEЊ)[apl//*3 s vL_$`kb%"> O.x~<[QcTE ;*zg'#JicG#[ΘSFZeU-.˜4@lߠ4uE#ºiP̨\ᝇօ{ZCMMRG',FRtm}vhԊf?[ۊZYR˕{r"q *g+ pڨjY<斕bonl#CJ0>fq)X@ L}CZ`u=#+&(I] =sk>7]T؋vfb]_xGTAs>qσ+) QZoo-!oV:Љ'мqI7 -"+HX0[jBtl$8]0Ë=.co r;+>[r|YPGK(Z`L TV8<7Wgkpt8p#ƭkDAhCoY*vΉ]W ūbkr_D3zX?HI8#nb»k"霶;]\vKѴTn 2YZIaXzӎ;N5_u[w3 T݆^ 0*gɬoR'o>aWEKb,Q)qnAl-PV|+&tU,+(dMKDUwH5 ~qWpTu󒿵Ÿ Ŀu*Qr{*bb_}h)%h":zc)elT!X\|I+O73~1#|! lV"cxŞJAAYDV6HF 5՟<"ME:)f0B=70D0rWVўcǎIKQTn-YγS" kom+˳VNjF9К0C:UXv #b烙\bs"&5|^{5L Iff_җg~g ȟ[[Vڜ3=g)F*YoV,m -ܛDqI0M`e?7Ex.Z0v8E_bh,\WϨ#[81?{'k|#VAJݻthB:q΄j2zXuMQHeP^Ee-*"9٭G('̰¹ϕapСBڈyo& sFao)ƹVK A-'~ ۔`ÂeQ 9yΈ3Ϝv%޷d%n`Rx]h:Sk-v.ϺK{}Kj>ߞ`\""`b*ׇa{u @ hCtMrU),XSM<+s`%)0,g5Ϩat לw ER ,wl.Q G-3T FKxff\ " X &v40j(џg 8ti$-m"x4ʼnE1 F̻$ꒀ7)v >)0$Bo?Z[|>,^]k΂N de I>Â`*esO^~ogGP`s7[w]J@X2r %u2v؂&]1PS]YwH?σRXzZ*{RJ>,`F SkzuKBеo,bXoBT?WtF0m A"Kr l*\Zy` IDATiu^f(e_3?iz'JsaX 5>l9sr劽זbDp6}W,~(\׿뷨0PB]ʔGEAIB}>jh3u%[GMOqj}|[/15ꭼ1^<3rqI@F!({0\QW@]a>rt!ʁ Ȁ2Ӝ-(UlA`FV"Te4;fɾ]zgS1=c>bⱰGV&?ACVz*ZVvEPGcTj& 6+>NG> l5pLj4SFlOx_}mB5 id}]=+8v؟mn -AyYz,D" >ޑhLf,a) FY[fbEkA C_Yb%LόRhnܣ+Hœ]qatLcX{}W!"y>U5N\yLZU\_Ɓ*?uf69vE E_|ԥhAWkf~_Uyk!68+v-1]࿽AK`ʯdVEXNbQ}/FPq,1\0#hN}|dk29W3\&JC,qr<+B(/+fV[`Qow˾-X ~=9A90JMTF.F. %SDvyK q_8bLJJ*TD[;.nC.TbY5#YQ쾃fʖOYIXlh4zFtSi:t3fLұbs/5Ínrc0cEB݂a?*,PA`?33Ao?2F?3g \^L r6Lu A$"a8昂|Ʈ\bo;i9PѺEsT,^1^E!N#uB 4r@̡ ICVm^e? iYчۀ]_Pnq#^^4ѝ {}4}s=AK`lJJoA@"?df47"QpS>QXR rr1Dd[h`;}>DŽ`ȝHfܘSH!T}ۇ>vKgϞ?.VD3bJUc.-+u$82HDqܦ:!Kf ʲo-}k>XH" u qaX<[**oh||;D,ͺn7[-ɓ_J>h"[054VtG֍3~c5[fẺ췬ue$BN )mc\d3vf(JQnZ}a8 X="y=R=en@ٜyϡPr, ? C終˜x o :Y:!`gt̟I~;-FJri|KኊRRxǡ;n3d)(`a/h ү9r+͛tKh92|C~? pE L-$*3E^_%[3 ;|K)_0+\#!?e^CwGA赿YYZ!v_rw;lÏl4L}JZ le9+`Xg]]{)SSGYL"寣wK.gL>=h4c. Vt|qHp;K~uB Pk<|oRq6cat(Lۉ%DU8G6Q-~dS٠$xо$}|]X{3r7LW\w]3_Ks}WBW+p=+vч[wsed"@ʍY Jeݕ^}J"#F3߅LiO].jT)dȟ'N|jrI7 WQZtP9aqY󡌰R.{:X&\A)%A9BHP&vc xc ̄ #]M9`u{?tY? s]Q|Z={,oFb[ HP C\Nvc2`͈(|X~P΁軈 hO{FE}`M3~/ϬDn#ff~^M-]0*&toNp!E.BgE!23bp [ _se3Q\"q. oUXqet}ه?mvK#y|!RCzXRT!luf_oW k~ѿhx6V6?;Pt9וZNVf=g啕܈>k?s4?{ ޤ,Ȱ4`Fx,6lh ԩ$Y40D"CF?KX0t}0O:^-K|{_q>PD ,z}Qݪדx߱swV˾(4REx9>"=NQw K3N {>i/K|S% Kt}%@.華!ze~.kSR?Ւ-=JǏOJYfh[Ru}H Qs Y̘#QUzE:Y6VFbm[1M'c_r`dHf `(ks$F4se{a鴱ח\bzod2%(h #שx=UdJ2I\ς {/י%^( ZK"O<JL~դBڊIߍ$QIX,kA<|?^Вn)-K}/l6UzHupcm$t/~jʀaop5؂B [ssB n uH\'LL8u{… /e]=cOR)!wӍOX˜G[<(!/5.GP 1DeC(5PHJ8QuT|ouOuUkCk+Vxs?)gKST08q}fodBlq;9t+pKe %[T٤-/STH!|G_t9rgfzڵ`c q(J.{}_> -aED9Nډ L~W{[Gᤨu 8Ǿf%t^Ůvz|Ƭ)EYEj-| jK_Zi X?![Ҟ)}y|i~c,6~ DKi0a`B9FLb-tyQS7|~Di)DF3!sCYaaK?-r)lg?W`c߰f*g՟ZX?la/J\A)=5x;Ċ%[01NE$:RI1,iRtԩ?3( #}ApV`$d4"o> %qm޶%rZ*/J) , -WU/\kӂqT@SaBxO֙yQgqT=Հ`K^}`RC#}y޷ߔ}_PSCBcq(^rhMy.>5+_ԀH{ )؟kH)q d 8( gG/*|?f)u?͒-?~9p@X8YT7w3RaQ L#;y&3LS[5V7_ʌ_qprc`z\db{]|y;G>Q{]1pO}5 ׂFrWd$B?xwk`~"u,LT1l0iGeW4A*l]ӂy x;v͛hLf1(FdL+6^TfR@ "hΨv_ð9r,&QުԊ8 Xc}|`N'bf߸ @@?0~7Ξ}nsC<@{:- ]( e dA*2=4A#eÕ3qfAWk" #T]{ٷq#!?w. vksffOcciLرSdM3aN/i6U6 Ӗ&`^`wP#ӝ12n Xg$J`)q f>߽Mrh}}_t7a(癭R8[\8.JŁ9dgt#DhȪg4I oFHjAǏ?lKzYRxxSf\cc9و1SFqe@xݗ 9}҇?Mp [Ns6bpZ-K*[ u>N9%~>?y)[ٳ*9`jubܩObs7m+8m8w(ѾʲL(V08#v 7ڈRtxVJQDf05dr!%h4]n%Z*{ѣG/Ȑ`Yp`@l':)]y D1Q`=h83˩~Jh,s}\EI|ӨhẲw]G^{ {SA[1l 4+- Eu@|)YX p! i.h~yK' HZy^{X3m%}f9ryV?kIvzi>O_7@W28UN[}㱤vȌmYQ:Y-ṉ|v=`vNt?݉8 ψ?95(q:i<,xa%[RU qrW+n{!w;⟵?`tGGcd=+E>LQ606ʐ3(%ur7Ff?裏MozӶ+y)p-=@W)?h寐v ~Wfa,qj?#9 s@^됝Z`҈VX92ʘsi?8ԁ3R\M`P![Tr|(UQ罆YP`+Wk֚ϋe>n8Ԡ*ɨ#a.N]N Ucf<~(bH`<)Q!jk,WyR97@ϭ\*[5dUuo3`5i zZn@W{3KiAj7FE cO:IrE>ONgdg6&4" 8?:,Zvb:vWl83-%s~Bg k 8Kb-"ױ훠 V (y|kR 1꾨Q)k芭ϡ@Hb>wmZƲجy5[U:5~q? IQםc"%:Q8LMϖ}2WDd YկcЀ$}.0RG2u=ɣjYw:p&Jc+QF"%a W?{&HQJ.cF:d*~6mὉFEt2fI~vAH)MӍԩW>+--HZC;>s_> w K]a B t: ".@0 }KM kʄ)*%|3\`TB!TQo,BIk89-I9Paa)̫D~N.}>2ȸ>TBw!^FfڤP=׉G 5T޺_;.oweeߗ}+DKi/m?o!!Ƀa% t% {&ICB SX@@/(BNcu?,[V"-e+px E*b8 WpsAcE>lyh) <+Ѩ}ן+jwV#Y8C#~D'yY}Q0s )cG,zy.>e/*hV Vr΢aoЁPCJl9GGCo2%>r *indf}>u)䬖5 Vú9ckڥ+Da8p IDATl@dxGL=u;7c0UQ7gb%-eJ]8:Owra3PO)Y1\;u-YUUOCwxfX݌@BJVce+SV2c2Ū>vwS'9?y+_y_=AKR%1˳>{oJNtcx"U f#!}߸BqFshV"EE=xyZs۩(dzWI0 H5Q(p?iO)hw.0 ak\担* I. W׫O xMxv[\žΈCpeyp"J,ߕ4Fz,t?2ŧ 4/Q&i@ipՖΪ_è+ F(ݗ-y͢=ƻ,-Mkկ~>RZU˥AadJ@!QF"M`T$S\Ӓe05NZhG.F")e9\7yF,Yc귅k6{O҆5>32j@s<ŏcHT> 믁umߟ3j3P RURq۾.}8!l-?XkM̙;1h%*PGn45wy2O1uF̖3-@ʼր6FY`0z&ŘLEi8!m6SN,>͟zi g~@$< ijMGB :~d^q29@ zNsHrY O+:CgXzp mDͥb_(XQ'œowYŅ ߏc3K6~6{)|V{ \ek~~r? !J\䄭hDj#5/I&!uTGsCPFf(_DAQP"Jr B *ba[jC`;$nwFB_W5FwAmbv(3DGs% v`CVʐVF~(`pwx/7V+9ANu7Cr??3Jׅwh#|~з `lwK<wyS,hԒUNJEW ZWa9q.w+O>3y0V#Cƽph~k+,Ձz1?H#ű}-We#Se@~x>i~`&B;gz| ~~^Pg=~̟(( t@(Q(1Fu1 }%jk)>:-ADPw^lJ_/as\--=Jr @${~m4W>lGErVUR,0v1.L݊|D֛%@1u%r|Q` ۭk.o Eψְ0ݲi%Vx^=HRh1F_e+Jq];]S<<qXSW# Vmq_n<3GvԯfG߉bJ\O%*Ar혒\`r _<ϥ~`Rx$b,@{q8h^n xWQr%7|߶~SNft#u=` T@)mxOud!xl63$ČaN#Ǖb2Z>X9V ƕ ղSߺ5# }`s|Cfi`̩#hj֊Un kVX9mnUYU B,TTآ:f'tQ \-|> u>ߪPH.luW)ꂂGw2iV@3cPXSfX@p2`Fbk-ޛhSl⋗;x_=-)}SnW>]3W~}Ϛ29CM\"q_]0S<-N ᪘˞mZf'qc@|[e ߘ`,Z{u9M]FH0ă|nX X۪:yn}P,ݽs6ͭ ovx?G#ICg.u"V*K8" 75+6v?kUcfq=ff?qǫ>!aҞt |)[(_;]77+++XG{gBP8PIUNriEj41rNrOŀ>. l;UR!Yuz EH,hmGH4~ l=1Ye$1pE O(`3l-sgMݒdhAf5VW§}['<3EsP~fD=(ROfTRc;u\ktœaٶŸQgr*A= B67Z]A/oxndFf$d-t h@sd-hNiÒYY!?[9|+g\l,@aGt~ ?Ư?Ъ#"i|h4a_w7;mR7рo*,_ſ%}׿K;)zuQ0żRucFdN1ǐxjW ɨ]<#yDB.7._ Ћ ۑkHCiZiy>sP=~-#dpvZ^{"z`%1WD1P UDeyO=~q[[W`egVve-T+ڳO??x65>Gu߯F)4 QKy>OYZy1ґiaWXo\qָQʎ}[YµRh)&8X |58D }@aҐD㤿N\ýyњ&J*Xle5Vd_Zt.Ew'?5@ZhQ=)*6ͯ~k_OwF8qkvRJ5^X F~Gzm=N,(RLQWڈm@(yFB~st͸0NXCf_γvh cm:{H#_!l?"Daz.=*~o.REv{f; &"ա?_Uw.Rh=e.@_1"¿Kk^^tr9$3]sX`0*c/sww-f湿4J "Z~?3[[|}ͬ#0aOug/os[<?Б+yph4 B<6"tds[qejut< W LG8d;몓N˩m]Mx]ו/\UwO] |)Zyr x"7yoګx}0K6!/[UVLQ^=] j/ aBgso䋏< ( sD?[fC,Dr,@\,-4 E(_Bo~+r~n~_t͵\ӽK+D^¿eݝ֫~77't:+NK5 &fq _#BV_3PPn!aUq[`V9l3So,: (ݟBWsxk[ ]c?݋q-lsWE+%* f.{ -pkIa_WZ5b~G)HEu9( Yh?~P6"cګ᭻i x){uIUݹOݯ_(6 b#h0LX$<\d#h&.Q1AdJąHADhbB`d125h0c[4t@BCwMQ~>[n;{n9zܺ}gNN>`tUlw}w~aiikJB pZK pBtdɭ.,AƗW^TD(| r3 [)/ IN"۪$=/ l=1ș:t[://=S1h C J]?c x{Y^;"[ň ?Ɛk![Y[ zG|sWIs~_A=NB4u?xxwߝ@78\'C\]HGt(m0 sɫ7t؟h4ݥKlVa <23,_@8$ȹMݯxy 6N.mnjVwu %9hQʇ2;},HЗDz>E:$'>Jxt~fo%ߗG瀿;H0* b^#?ofH򢢘)gݐu 3AwuJ\$a7&_yh? xi,34>zg?{b{A)ۡ::yd,sjMyi˲+׾57fYܢݚ-,䘺`cʮ{|#,z|} JPcr6 lt-CڧYr{;%IY e<;cS81ìL#SpbXnm\XND)x $h)9^4sYXX Y0暿:-oyqP,@l:Xg2OsC!UeGܲ7gT-:ŋZ^|JOˌi7˒:mQCɄ)I|6U @B"C_lClJh3A=Z =Fz\cVZ AҶ~{[o!.:zvMh,B,sLjtzW]uQ[`پXs./c_QNNf{V7R#T2OAd:9^ I -3>$rmA#/-}HDAnvr&}6u3B@~piUM-~y4ϋ6ߞreji/ǓC7Z>S;]| @$ހO$c=S IЇ> /żp5S(QSLq'_%UW?mC5(&V^m뮻7 ;Foer.MV$`N?uu,{_]*\yx+\?,<rrQc`.V- @+Zaݥ IcEZvWӼ49>f bAXE <\^a뷖Ga.O~|c-i}xAW5xߪ; MuuCSgg%8K-_ب~(Ds#wPիWCetu]CV~ހ&$ ֥ۤ$ Dmp1[%wrn4jȆ~cpr=ucyiWV.yib5/ Vt+Է:nw-Ķa>.}S!"<-0ddeHN,x:;|x* NGT4/jqҏ?~%\Bi$4̧Q37y_g.XZ:2Sr d` ~|)80P>Qt- ;/׬Ko 84g+F!:cCI{$168rz_FcxO?2l##<(QІ-]1KK~K.c4yĖyj}ct*=4UzI:&hW d7o{ya8 9`V/]B(Y92! ԰՗,e@[ADx+s-GL9Byl @\uAxY`$=WE_f{af{NxD[`%=] ̭a v-c4@6N~D'`%{ԣD E g SjӠ=vJ**/=>6jl=m / 8s+`-;&+.!A5mϑcɱk-^{Gr7>(LYeqؠE0dncP⸺DgjShr=` hlG2MV㢹௕) ,..zeo5\tr݇OOtB}t|i;m(`%IY`ʺiLY |Wڞp o+>#P,>~lh_,ukq~#AڕG23|q*`]QTq z],K  P|z,YGwڍ߄?2P_&GD@zza&(5Mb-!V=S0?"ja=e~޺rǂ,IqA0z IDAT@,̈́$6?NY2P P۸q׿rWE9S Z%`cA@0B@P2Rz5J4[y `928&mNEMhY9o==΃ )r x=pa9x%Gm88X6!u}5bg<8K ^Z x{8aIv77[X(V?v'wqq!%а,T6.$)Y? w>@4׾?]fD,S7o6`zan!-2(ǻغ\*X#xЙj5!>{C.K?d^wG`4 wrʩwL]}hX+wbt}#+y@SYNuM*:kn鋯]Z>L]t `-"w9_9Y xlC,z_\Z(¼4hoG>`g5kgv:{PiF#t9lhE"{l>(N]`i3rz_ވ6hG58Ee?qp<:ld%:\sq Q-@Zj jE_tA3R2b߂ȃm Gø-&.|oKHCIm\7${C"hg?4.lGpJ:S 2*1"@H~ x?ѫ.|+_}kh}pA@W*$`Qfcӧk˂Ce?ݴ?}g>S_K~8ǑER s?$-H 2up+ؔ@I/0it"84LNQt\ڒLj\o[o Va8^["g.cÖAq~3#)0,\L %ugg0Bw?z_Xtnx}_֗iۮe(`%I^aްaW.۴㦘rw܉c3rE04sV8ݧ4r08n]GgX0luj6lz Ěf ' @!v-7 U;a&vܽO@iDwÞX{cDѸ$9_=6/}38}?OmXR2_}~LD=D";nO>wQӂ|aW@g mТV- f]r  I6/%DSRc2kN~7w7̉˺zh%B ̜ @4'(Rψ%5# P1"߾s ~ߐyC=X7l)e0 ~~9 SԹaso煋~,AwT/<{>Kn Y>KDǧ36N,3b8>w5[p@ š 9ΐѱ~-摨Ga%.E >ؿ]s^7-rh_ε#~N3ɹ^fNVПUѾcvVݳ5ܵoߴiӫڄnSa #,-@9݊NU χi#*-nu{1ea |l-+|h<)A<@ K )']^OBdW<ȁnmLolSH_4s @E9DCk< C ԃ#-ujkDG :vo\//riSvW_q90|{oy]pSۖ1 fn(,T҉PΒ,!M꒎&$H pOy+_/ٳ8fҁý4A.U-9(AxD>8ݍ6u5۠Ӊ~U3סh3 tc}. >`ߘ K{Y=;! mu+no;zlC s4SSy?t`0իW1yw;czv)]ǖyߦ<=vKP?! ʚk6_1ay5^{s3/0Yᢷ#@ZH~[/]<ЮG#Ɂ ITyx @32Nã02Ռ}Tx. V{'09Rσ;tiUw?_ İz_C6jcF۶m .x;vgmy4ex\xKӏ)oEfôW5,1-;v8S'?}@eH,Z@)p 0З90`7oXs0Ȅ{c,@ r3$S݈YZ,BoM>>OX>++n2q tXzBr&s~OУng ׏@iA=9ԒƄC u4rBe@r_XXիWCe⽟r'}`ǎIl#VBY{{!ęx:ئeڷFO'вc9fg.~ssQAHq<'Ymr\xG˽H;dĺY{1lzimba'A r:(C*r=?㌳ꪏ^{0e F㌂f  Ur?FōV-b2mLe`e"Vh|p:ퟎKpC%8x8=VANAVn&{thۙ^^) x1GI4J2%#_ ~_jdYw?ᷞtߺu>gCQꔃ6>Ju|PY]@l][ۡ$@׀ހk>_.]XAVX4 b,bZXŅoX4=/wCrgmj{y]u 83 -ڂP3 t$=嘿tN=LӎH@~" _W\_=^e)@Ygz4܅*x4pN ExjWM(:i|2B K^<Ǭ}.Ҿ-pC>_2Kl0vͷo񕷿oǀL]/u|emLv$0&+;|z.c7eÆ a[2 -%oL2}H.'NA*k{dH@) e8t1 *V=(p+^%+iFP=JVtk_c2(}_sR/Q\ Gy䮏}O|{sO?&i'0I"A3&5k,_3_v ֊9Y-v_a<5`TZ4`)NAc #0) ӡ<)|_7 c ȲrKN%Yә C۾ 4ˈz:C~i[VZx2( W\ʷ_|oJw%9@IpqZ;$ c  gV9='YT F28_ާA7VfXI8O%Ftʩ#PcawU(IcIAeɥ?ƵS@?W} mv:tdYLVWGǥ_D2D@+]!"=JM/;X/ޕ c 2Px.|/ Xt>p +<|h&]QJ k93;ǔu/ oDi_ -zB `J omtRa/)Gw7 KW)e;w2YS҉OOLX`]v.[/u途(a#pۿ[?3̂;sOgs{8!6׾:aDR<-U]5O GƐ^zO{| ɜ$M9Sɲo0nz˻}u_җvҸciSOI'ږ>p!Z;nտOfBfP"a Xֵ=.Z/c$π~|~wv׮]sgAg4թdUڇA{!6ňw z@%4s4hА)u{SQsidC JaX Y6(L8/r˃׬i!I))ݮSeP$K|}[h::1C ﺠ$00]h( :B2}]E 5knSUxyӋm U*Yyn Vms0$~Nu۲E>Uksq|ݷwy睻k1shMi׆k:>=m;TWG'~j@9+eN cCh>!m-$R?W_:Cc͖ `۠/3b6@{W) ?=3ՀE#&rBoDms +q_h𡗜}? 쳲{c~_w7;u@NXPk~B |:__.UI`Y҉'C哐{n; JNW`ժUw/8~}1&@-{8b8 ,Y9[.|O?"s/DA)r20g)e쁋hIygD??/Mkf05&F讻_+{iKE~GilwPVWGǥSފX'm:$ w0mB0I ^pFbओN:~>~ayA?X{9=@Pfձ{nAfsu ]C!hʧ,Vh/36 gϞGn7r-{]$]1{v}g޽+]:/cɀ^iۡ::.e}!0PڸWز:$ ^%nv̰sN;K.yᩧ?(O2kh@ b|%{yow^n~ n7W}]mַbK+?cڔ} _cbaCsOצζ 5Xpw :)mм/ҹGU:SO=iÆCZ܈YZ@ul}rW\@[Cq[Z| >?z-sbW9oLVD޽~7'>U]D@Hnzhf> oWNP]Lv/oe1dBqCe"6hF<4&@zB 4/QAe矿; X2mIDATܦM6gY1ulw-C|m/W1Ȃ*O"d wYF;wn _qo k.M Dt!&d[_yNB:BzJ^L_J\{_ .R } 궭fa]~?}k?p8O~zz)E.bp]I ! W^_ =kUOI W9iSXŰ{ݻm_׿cǎ{<ֻ-\A /'yW}MLD@@$ YNSRPcBt_0r@$3 1,eu9/~Oټ׭[wq?`}OLwBRђupd8G+{ٻcǎwqǶ/~?/;*\,Zwл4H|K>FO>`OP%f$ ?kRW@k ."huO?/y9| ^<6?N꘿%Ƚ޻k{߻}֭ P+I1۷ /.~X Ӽڶc=C [&s@ tb& ʚ\~wIb0 @Kȁ^ӗm\e9yk_xOlڏ~pi{k?{޾}?ڹs>AѓKjq_\H{,Џč rk:1eM]t|z:SDj hzmWbAWDoH@+F|h_cay .?}͛:#quvؓ;ur!<堃~ʺzҺc젃֮s5k.,,,,yk֬^ ``,8 `߾y> ygϞ}={ߣ#}衇=Ѓ{vz`}mڵk1rܜH>.Jt|eZ[YVUǂXlK(U< p)&1nB $ n)pi >>fTbO++LZ&sR hzJ?9G?tm}l}hSSԇh&+Sޤ,ܮ6]'@ӗ־L$F%O}q \oJ k/PԻaE{pYRQG_oK Cߓcn/\5I@yԹڥҋ% ׬b2/;^ 7)! M4K]kP<>{p]-,^/w Cq6xDGkف}ߓLnCmbCI"}+E﫫S>iYhg7p?ק e{ysK^β͍} w|\zz, |t ȶFiZ֤ "uBJ9T 50=yQK\r/]'2:oW]Y֤ozW],P$9T:$$u`|M0ڹ2 e_ b_:?>y|{ku>`;."y9~_[>hN]ݲc's;ҍ@:$uۇh\fYd\}*wzu^_gqYT4}@KN#_2PCdy:ߴw..]_Vkޥ a^ tKv=_QhgEꔹCВtbۃGU62XSuU[uvܳK |:Mtup0%PۯKoD`:V[:CV+}8Ws^n~^>rY>= C}_+csR$mB\:cNSyw"1>Vߕu]Fѻm ] Jy]L;W߾&/`,h ĥVEĒ}}t M^X0ˁ@$f McVdU>xd\]V\YbqZVKzbysmwuP,a?,#1>6x->֣%$юy\x4=ObI%VZZ_.w:Wl:vmZճP}>ݾ&̆1ԭoȾm1^Hpmʗ ]h)ibfֵQ_W/8!a+Ф`h3)I1}ƶum<1z>b:qHT4&.my>uAuߴnSDǧ{.!I?eY Ed4i:qkc}:mNMai 2u^. ^w1mF]Xn_Vd9?,g#ⰀMLu dEtAU61y |:iKKt]m |" muՇ{<~lP7,WNwkӻ0m11ly,6~6 uݖK{9Nҍ9%"TL W]nuIEC1@hr]r, @bu& Mtsdz|5YPe#oZVǶ#YqObub5v_Z]7L۶&ukS7$"+ a+3&0]S1:]ߔxY?mKx}:Үݻo cV"[YAL L2DehM]Y5M ݕ{>bCzuMIaxV2C"(sDbubL^˾Y}62-wov]6]ZӶֻ.+$ d_IԦIŶmx}i$@Ֆ =&Ƕ$CzD`!hnǚm~iJ-w?mo%.d@mr '.i1 ,ڲlaawާ? >2DjJHY}Oka}i34鱺|AL+r pK1m^Gz2dn Pl6m<]xږ.M%Yhv]YmuI\hAzJ` Aj1 m@&)jU<.I$}NG/A"@c2{bubmƳ 4YѤMc1MOHA:f@]I`s23"}MtU @6w=,OUH Al]NB16ڞط}Hn$eNL)XnZ1K,Ͼy2ږi缼0l9:B+6缜>\_CYrپM4>> Y@o%I"@!RVu%} "O%K"P!!,|IWf15p.%DV,sB@%PYCS_X6o%I. I$$`b@咾Xz^I$VHD$ݣJz$~$$bЪ{^@-I$u%$I"IL_'iKH2uI I>ɴ%$3D $I@dv@^J"I$OGI \J"I$ ̣$dJ" IڐI$dK" +S'Y@$5%~J$II"It(<%x$I"I$I$ l'$I$I$^H$I$IV$$I$I@I I$I$Y@$I$I%$I$IdJ"I$I$ D$I$I(`w}e{,IENDB`focustimerhq-FocusTimer-8581be2/data/icons/io.github.focustimerhq.FocusTimer.Source.svg000066400000000000000000005726731520625676500313660ustar00rootroot00000000000000 image/svg+xmlimage/svg+xml256x256128x128 / scalable48x4832x3224x2416x16symbolic focustimerhq-FocusTimer-8581be2/data/icons/meson.build000066400000000000000000000015361520625676500230520ustar00rootroot00000000000000icon_dirs = [ '16x16', '24x24', '32x32', '48x48', '256x256', '512x512', ] hicolor_icondir = get_option('datadir') / 'icons/hicolor' foreach i:icon_dirs install_data( i / 'io.github.focustimerhq.FocusTimer.png', install_dir: hicolor_icondir / i / 'apps', ) endforeach install_data( 'scalable/io.github.focustimerhq.FocusTimer.svg', install_dir: hicolor_icondir / 'scalable/apps', ) install_data( 'symbolic/io.github.focustimerhq.FocusTimer-symbolic.svg', install_dir: hicolor_icondir / 'symbolic/apps', ) if get_option('profile') == 'development' install_data( 'scalable/io.github.focustimerhq.FocusTimer.Devel.svg', install_dir: hicolor_icondir / 'scalable/apps', ) install_data( 'symbolic/io.github.focustimerhq.FocusTimer.Devel-symbolic.svg', install_dir: hicolor_icondir / 'symbolic/apps', ) endif focustimerhq-FocusTimer-8581be2/data/icons/scalable/000077500000000000000000000000001520625676500224515ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/scalable/io.github.focustimerhq.FocusTimer.Devel.svg000066400000000000000000000264761520625676500327260ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/data/icons/scalable/io.github.focustimerhq.FocusTimer.svg000066400000000000000000000264761520625676500316700ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/data/icons/symbolic/000077500000000000000000000000001520625676500225245ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/symbolic/io.github.focustimerhq.FocusTimer-symbolic.svg000066400000000000000000000032641520625676500335500ustar00rootroot00000000000000 io.github.focustimerhq.FocusTimer.Devel-symbolic.svg000066400000000000000000000032641520625676500345270ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/icons/symbolic focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.Session.xml000066400000000000000000000223651520625676500304230ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.Timer.xml000066400000000000000000000174731520625676500300640ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.desktop.in.in000066400000000000000000000010431520625676500306520ustar00rootroot00000000000000[Desktop Entry] Name=Focus Timer Comment=Work with regular breaks Exec=focus-timer # Translators: Do NOT translate or transliterate this text (this is an icon file name)! Icon=@APPLICATION_ID@ StartupNotify=false Terminal=false Type=Application Categories=Clock;Utility;Office;GNOME;GTK; # Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! Keywords=pomodoro;timer;productivity;time tracker;time management; DBusActivatable=true X-GNOME-UsesNotifications=true focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.gschema.xml000066400000000000000000000142111520625676500303760ustar00rootroot00000000000000 1500 Pomodoro duration Time in seconds destined for an focused work. 300 Short break duration Time in seconds destined for a short rest. 900 Long break duration Time in seconds destined for a longer rest. 4 Number of pomodoros before a long break Number of pomodoros before a long break. false Don't start break unless manually confirmed false Confirm starting a Pomodoro Don't start pomodoro unless manually confirmed true Pause Pomodoro when screen is locked true Announce Time Running Out Notify when Pomodoro or break is about to end. true Show screen overlay Whether to open screen overlay during break. 0 Time to lock the screen after opening screen overlay 30 Time to reopen screen overlay false Use dark theme variant false Open app in compact view true Toggle all sounds "bell.ogg" Pomodoro finished sound 1.0 Volume of Pomodoro finished sound "loud-bell.ogg" Break finished sound 1.0 Volume of break finished sound "" Background sound 0.5 Volume of background sound (-1, -1, false) State of preferences window false State of global shortcuts false Automatically launch the app when you log in "event" Trigger true Enabled "" Name [] List of events "" Condition "" Shell command "" Shell command "" Working directory false Use Sub-shell false Pass JSON input to the command true Wait for completion [] Actions A list of action uuids to load. focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.metainfo.xml.in000066400000000000000000000117201520625676500312000ustar00rootroot00000000000000 io.github.focustimerhq.FocusTimer Focus Timer Work with regular breaks

A productivity timer that helps you work more effectively by breaking your time into focused work sessions followed by short breaks. Work for 25 minutes, then take a 5-minute break to maintain concentration and prevent burnout.

Key features:

  • Customizable work session and break lengths
  • Screen overlay during breaks
  • System tray icon
  • Hotkeys (global shortcuts)
  • Daily, weekly, and monthly statistics
  • Extensible via custom shell commands, D-Bus, and CLI
  • GNOME Shell extension for deeper desktop integration
focus-timerio.github.focustimerhq.FocusTimer.desktop focus-timer io.github.focustimerhq.FocusTimer 360 pointing keyboard touch kamilprusko@gmail.com Kamil Prusko Kamil Prusko #bedaf4 #a4c9ea CC0-1.0 GPL-3.0-or-later https://github.com/focustimerhq/FocusTimer https://github.com/focustimerhq/FocusTimer/issues https://github.com/focustimerhq/FocusTimer https://github.com/focustimerhq/FocusTimer/blob/main/CONTRIBUTING.md#translating https://liberapay.com/kamilprusko https://gnomepomodoro.org/release/1.1/timer@2.png Timer https://gnomepomodoro.org/release/1.1/stats-daily.png Daily stats https://gnomepomodoro.org/release/1.1/stats-monthly.png Monthly stats https://gnomepomodoro.org/release/1.1/preferences.png Preferences https://gnomepomodoro.org/release/1.1/screen-overlay.png Screen overlay

Overview of changes in focus-timer 1.1.2:

  • Deeper integration with notification servers
  • Idle detection on Wayland
  • Improve screensaver detection
  • Fix autostart

Overview of changes in focus-timer 1.1.1:

  • System tray icon
  • Smoother sound transitions
  • Fix break overlay scaling on HiDPI displays
  • Fix missing sounds after switching soundcards

Overview of changes in focus-timer 1.1.0:

  • Support for GNOME Shell extension
  • Option to autostart on login
  • Reviewed sound files
  • Fix build with vala 0.56.19

Overview of changes in focus-timer 1.0:

  • Fix break overlay scaling on HiDPI displays (thanks @scholzri)
  • Automatic daily backup
  • Removed libcanberra backend for playing notification sounds
  • Updated Lithuanian translation (thanks @psukys)
  • Updated Russian translation (thanks @ViktorOn)
focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.service.in000066400000000000000000000001271520625676500302360ustar00rootroot00000000000000[D-BUS Service] Name=@APPLICATION_ID@ Exec=@BINDIR@/focus-timer --gapplication-service focustimerhq-FocusTimer-8581be2/data/io.github.focustimerhq.FocusTimer.xml000066400000000000000000000047501520625676500267770ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/data/meson.build000066400000000000000000000052531520625676500217370ustar00rootroot00000000000000# D-Bus dbus_conf = configuration_data() dbus_conf.set('BINDIR', get_option('prefix') / get_option('bindir')) dbus_conf.set('APPLICATION_ID', application_id) configure_file( input: 'io.github.focustimerhq.FocusTimer.service.in', output: '@0@.service'.format(application_id), configuration: dbus_conf, install: true, install_dir: get_option('datadir') / 'dbus-1/services', ) install_data( 'io.github.focustimerhq.FocusTimer.xml', 'io.github.focustimerhq.FocusTimer.Session.xml', 'io.github.focustimerhq.FocusTimer.Timer.xml', install_dir: get_option('datadir') / 'dbus-1/interfaces', ) # Desktop file desktop_conf = configuration_data() desktop_conf.set('APPLICATION_ID', application_id) desktop_conf.set('APPLICATION_NAME', application_name) desktop_file = i18n.merge_file( input: configure_file( input: 'io.github.focustimerhq.FocusTimer.desktop.in.in', output: '@0@.desktop.in'.format(application_id), configuration: desktop_conf ), output: '@0@.desktop'.format(application_id), type: 'desktop', po_dir: '../po', install: true, install_dir: get_option('datadir') / 'applications' ) desktop_file_validate = find_program('desktop-file-validate', required:false) if desktop_file_validate.found() test ( 'Validate .desktop', desktop_file_validate, args: [desktop_file.full_path()], depends: [desktop_file], ) endif # Appdata appdata = i18n.merge_file( input: 'io.github.focustimerhq.FocusTimer.metainfo.xml.in', output: '@BASENAME@', install: true, install_dir: get_option('datadir') / 'metainfo', po_dir: '../po', ) appstreamcli = find_program('appstreamcli', required: false) if appstreamcli.found() test( 'Validate .metainfo.xml', appstreamcli, args: [ 'validate', '--no-net', '--explain', appdata.full_path() ], depends: [appdata], ) endif # GSchema install_data( 'io.github.focustimerhq.FocusTimer.gschema.xml', install_dir: gschema_dir, ) compile_schemas = find_program('glib-compile-schemas', required: false) if compile_schemas.found() test('Validate schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()] ) endif compiled_schemas = gnome.compile_schemas( depend_files: 'io.github.focustimerhq.FocusTimer.gschema.xml' ) # Bash completions if bash_completion_dep.found() install_data('completion/bash/focus-timer', install_dir: bash_completion_dep.get_variable( 'completionsdir', pkgconfig_define: ['datadir', get_option('datadir')] ) ) endif # Run required post-install steps gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true, ) # Assets subdir('icons') subdir('sounds') focustimerhq-FocusTimer-8581be2/data/sounds/000077500000000000000000000000001520625676500211035ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/data/sounds/CREDITS000066400000000000000000000015041520625676500221230ustar00rootroot00000000000000loud-bell.ogg Copyright: Dr. Richard Boulanger et al License: CC-BY Attribution 3.0 Unported URL: http://www.archive.org/details/Berklee44v11 Copied from: https://gitlab.freedesktop.org/xdg/xdg-sound-theme bell.ogg Copyright: Ivica Ico Bukvic License: Creative Commons - Attribution 3.0 Unported Copied from: https://gitlab.gnome.org/Archive/gnome-audio clock.ogg Copyright: cmorris035 License: Creative Commons 0 URL: https://freesound.org/people/cmorris035/sounds/202932/ brown-noise.ogg Copyright: NoiseCollector License: Creative Commons - Attribution 3.0 URL: https://freesound.org/people/NoiseCollector/sounds/3948/ metronome.ogg Copyright: digifishmusic License: Creative Commons - Attribution 4.0 URL: https://freesound.org/people/digifishmusic/sounds/49115/ focustimerhq-FocusTimer-8581be2/data/sounds/bell.ogg000066400000000000000000000233521520625676500225240ustar00rootroot00000000000000OggS"L.vorbisDOggS"!Cpqvorbis+Xiph.Org libVorbis I 20120203 (Omnipresent) title=BellARTIST=Ivica Ico Bukvicvorbis+BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s6pR4XhJ 01Ƙr9s9H) tNJ)=Bz!B))C(!R뱆N:k!Zj2(R=PRj){K%ZkK*)z9RL-``'EcbC BH)RJ)c1c1c1c1 V+j'tfdȥTD#5b%ء`!+2Q5^+bj, AAe($)XSȔRY%tL)F)BƔc)tZ=TJ @P` CpK(0(I @"3D"b1HL`q!246..tqׁ P@N7<':xH6hf8:<>@BDFHJLNPRT> "9@@OggS%" nmLTWSVtV՜=_t8>Hk\5DeF)R )]d@&!hoql˿ǎMmvSm(+ԛ[oܢsI?}>\d>S>7}>KFJ?^x^qMn?׿>~= "AZV*4渏 ykKn~ma|/N·OzYZTs |P&'B[[qrӂB^[ZybTO:J@ޑ/'p6OcxCrS_g<^9j^:<ץ^lfkۢ"hviMDsyweZ˘{r'd?zodGk5][򹯸/e9?VU:$?tzۮ=*ж/~ʿ?-_QYM2aɣmk`F95Lą9rK[YgX6s'fb35כƌm}ZOjk?Zj֫M\m1s" 7V#D㼷ff>cgy Sf?8' 8ؐNNҋR)~%""6z?"8zY ̤yy}.iH Lto>yWq&[ߑtKڦ6M{d4~g*>rYsOsXr5rMLsܘLdL;ɏ $lԺQʲn ~?;GNJћ#m1RKDq"g; m+=c+D5zApbލL!a_>qk~眵j}b9L[?{Xn"gd^1V&d,-Z\f.V*=rѦmX Cww{~k6ߩEnO}2*/Old_xqU|oyz,YiRJz;A4VI{I?۸|Xqp0݉~OwYʜ揭iao8s% ?k~T{VYQYQqxo|JP(,MBt#/fM .{'w61 |u5YM :m< zܸ>AZꃠqލ_+xlDEFcȌ3b]DZIu8IX"JufUD[WՃ}k5U6iANK> 0s zjq\z sFZEFIbFOq͇1=ʉbbP cLzJ)}J4u: ,'U$] 6v)CL_K9Lsao׵AUNǭ_7D TlrbܿdD^QL>Bkq4&S6oz>ckrV#pV4{N~:Cjq}Փ' K0Kn9={n'&l9 ev.䃒W,c@i>5'L0}d2Q%3imرޟ}l_[l#`uNX=GWqX'o|Y^Ox{>錄nlzz*kOSq̔5VG1tWlg$@=9࠙ǣ|gWR/C,Z͜=S͐L돑{_u.%^UDƯxYC?9J'gs/;"ߑЗD+sOh#⃢OYwm/ȦBG?΢`s}c^:\bO2EaקּzOxjtI4Yk;/ɻqi%=%6t`len[Woy<~h]~`?槒rvaIbo$0w(.6i;Zd~%`?1xCVp'ؘP' +vl%.&JS Ukϊ"<$N'%%YwǡŁ1:b_S-mou9?*,ѱex7ĭ+:O;ծs]=phF+23.5ʿ:ו :](:!jCWwR!Ho=SJ*l!Vb6n%ܳ=%p?IZ\VO߫sG\i?(?I\,e)m٩Ȟk4D#v .ؗ$C.~kVU+^J>>F,sS_K7 N~[;MsIrrGg{?L*ۻ8C& ~rDbDgBuO4$9 e@9 \<ͬN\LkZ"kU;S5mpnG%֐l>lT>OK ۂ)_0͓ >7֝,r$)U-q7=MJbq9OUuW?+t-6 l3"ߺ?>+60 8uOVW\% }8x3yT+yO酕ꪜZd]z|oMj,{!lgwܯ^)򝟼}K"tV,Xn–%_S,μ,dF|^'yiD}ɼ~ǸGRG=IbɗPRT" bw.a9XDZϷBmo.ӝIXUq?) rωn)q_ 7~ \<c/պ>qj5DN`=e`JuMd,*OggS)"~5ԯ/9ޑjw}^VxX\ykJ\;NH`4'K3 #PC}]_Oq]ܳМueZM#0uZփti&Ǫ˜:r 󻔵)t;uhفSkyyr Ón(G)/Xag~~\+*/zFQqzAK;\?}?|, 9UM&wxZȞv&CZ.~Au}?Zy֟3^PV+V4٬wXO\VE0ʠ'22RM=<>.$$"wU(;-[ 3m"Uv-j^魰j[c^1ewz/X7̏0focustimerhq-FocusTimer-8581be2/data/sounds/brown-noise.ogg000066400000000000000000007043061520625676500240550ustar00rootroot00000000000000OggS[evorbisDOggS['qvorbis4Xiph.Org libVorbis I 20200704 (Reducing Environment)TITLE=Brown Noise&ARTIST=NoiseCollector of freesound.org@COMMENT=https://freesound.org/people/NoiseCollector/sounds/3948/vorbis+BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s6pR4XhJ 01Ƙr9s9H) tNJ)=Bz!B))C(!R뱆N:k!Zj2(R=PRj){K%ZkK*)z9RL-``'EcbC BH)RJ)c1c1c1c1 V+j'tfdȥTD#5b%ء`!+2Q5^+bj, AAe($)XSȔRY%tL)F)BƔc)tZ=TJ @P` CpK(0(I @"3D"b1HL`q!246..tqׁ P@N7<':xH6hf8:<>@BDFHJLNPRT> "9@@OggS[Շ #(DqZ?^{o͌-Zk+uuMvd{c~-K9Kru_5ϸYʄY(H^"O gӲT.XT潠굆!)M>-/z*W =6߶9l>N#0X&MӚ\G h}cQWW%w+O{|4sjwSdw6FxA3J5s4n`Ǔtq@Ǘ%%؞k}2\?.,Mo:'yS}]#k~Ӻ(> 첉` '-<,O؟fzׇOydbT;o=rҞ߽Mr3~1W/_a^Y}v?Lz!lb'A&w#Aw|}\c4g{1u{tLF@wj{6UFWvZ:}=X;k}>{-kc y:p=˙`,'uںљ⽘׮py׺&%C33iT"mng0dc~i6_ژ'sw\& bel8'ͯ}oC\/~_s3"3׹wިZqߟgGo$}񮔀L\YyG,~1jۆ8K;2O,L™9| w^\ _խ?{ M9hyÍI%q[G5f[\sLSK>1 ht&kU]YjJIN݆-|MüϜRnKP }$&{}F!?%;9zB2}=qy=$bp DWKDqZG\*i$n?"LB.of/m EŜr:DDKBIaֻ>ͥϏ^p5;J>?\ybJ[ݧ)}GPɹ/75)gsYPLA0w%scX6+mJ2;cGn(8u87Nnǁc]y7Gݚo@͝cKyhmmfʒع(N-|>_ؑ2KyEk/Ȳ Ad`^zpoKsj+:3v#sv={kp}\B C$Ox]3t=#I zObq{ v}|ݽa\h᳂y~ɞ/MA2Lj2mnt{M0ϟ^:jdSg\,"6tw+Kf7?f.ZK?d<ȸԵC-~_[/W&!{PGXYyOdc{hf,%!yZ@>|92A44Bd5W2of\Jp}8'k0 įT|<^R0A ØO%@VUmfv1^՝O`~M01# -7cg=sC5go@S{1pmVs/X d0z?|ugk8kA_WWBwz%řsEH7d\K_UV*:_y,WAD>s5mܙCTyms\NSYD1|1j_e!<hIL/;NAsy;NSkI,U%a?BMaݎ,A$k7PH-H9.OFyZ>M}twev|Y5TҹSrtp>11 :S׀HMNf2\V]@[eUbTh;sv^9v&Qg_Q!AYqg+;^3Ҧ@I~:g} oZƜn*mUBܑAۜ~~}*+v k&+{<:TV$2qR3TdO~ s >zzLރR4\yۘyns& -~w0&,o 6:]n"dBהڧ~S/TAyv1(BB,Du*MlVg~>o™kea14j ِ'Cmϛ۲d !kŪSg=?R<r_cς-G my?1:VGCu$D39?/䐿_;wԜljOD$D~I|X"R u%d)gQ|87z}U'&ԗK}ί w%]zV~\=*Z^z8pg\Nw܁ˡZ5_3'# `v|@S ҔTKߵSgJktȋvYoaM*ym&s9Hza|t** {8ֵ*+}Hsj|te=4ݵXޯC®ćDVw׺>btkr@G$a]h4ϊ@쳫tcxn[#16k0?F.̬sY.mk\/>{T^k!s~z樺5\ğfKoicZ2bzF:#ywid%AMX-ksh-Qؔx`bY@~[a4yLvvn:μ5Uw*T@OggS>[گd ^u<bS@] k1b59y ]n%I _?^dhjq>@n;[E-rf gG/˵;|u4̲CӍ`Fp}C߱?\ʋYz0 lZs9V4/V`2lr3gSό?+}C Ryc)h~#Ύcqq/.xiy\~E_bx~3v[ 49~_Ιs˵o8w6e8`>N?~3;JU(d;to<">օ%!Gx3_fĚIl_)mJ?^X(o !w%W!EẬM=Qi/~~|wZm욀O#;.}*kg]w^CT^=Xd{лP 9eЈ*<9$K80=(oOZ'اOͅOZ<֓7ISKv ^iŘ 쿑iue7T$fz~|ך* n0ەi'!ff/y!i\oϛvpynꪪj΢mL٧Msi5%Z->("z#Xa|˿b8Jg+7pm ᐁOZִIĶl&>Kc+T|"BqRџ}X _yJR?nc/:~^91A=tƻ]?3=a狢dy#Z{"O].-mO|?%aݜOcb2we>H+Oս KyǞ&^rˉu{9±9 (&p }xl^[y:Nϡ~%gN肜tOOMm_@_dvhysxgnlΟpˡB<- eۑ^gG{)" Ug>'^,LLYs- [!໭kªHa;߹=Nkx^M󼮪JItx9q|O<-dɱEUw3٩ $"P%7ӷ҉o~F䰚MO+wkweO'[;Y2:]7)0^(z8g'>vrYjkZr_>kD2:ta3+ն?('LOK9zSvXus֑ߘ$:6ՊֲML:/0'\:-Mfrބ@Ҧߓ{45hk-8gN\̓(m}LZ?s>'<7_q1w7ՓebwlFx"+hm;n̏} og#n71WkjtsPA3 *p[%&@;(j>?>=FML|\p"ܼxryb {;ol9zkT$?I/s\kSj/ȺL56?جF~|~l )Ɵ"Ō8 g̈o3-Sxfb mĮ) o&S؎rC$׉s5J^7?圶MAК&|_IҹHܯNo6sDrDmL468w{V[$I33;A&dԽ5Ib\C}@o0O)pZB _LHӄAm4lR`oZ w*uT~M), zNhf5ĺhV0w=~vl>8+,1 <6S2jW)o[wyD2 AJIcSs2[`Vyx}ɝɆLa)nL[M/sq%R>_GM̯_g}փ!<ץ~gu-/F)u{D+.vϣTK|MK7W)"9>՚0;i kWaݯCѿo×˞.̛FCyҝ1^ Շ7%q5`oWˬJlZ@z}o_ؔôrcn5ڲo,rp@|d3@=LLCCdB\}\um |8_S^kU-' ;FS&~|SÔۼlS῾+^^R}R• )1Bk@MxOT*wo23obMYOsYye%-)`|SSIS$Sa }Ӗz?!#,_|Mp=(eh<7ZV^eVPٯK眙|?,R!Y?y^vTG3@"/u߅;=NGw&o}&ޯr#7vr:7)?S@Vˍ`e姄 v<%>CF z_?2d 'FmD']~#~<ҩmdY)_L_R`6xB>+nB@"ޯڦ ׯI֎.ya1ְn=GC{>@y=ySō@@d.)}'oYp{x)qQ1_=tFk TM`f~u|E~syE,'/L۽!O Q_tY{,-CDkK^|CaO.~@/6] 49!kBux?(C(?AEgcO3w͟Nǃ1?:wt#:d}"0_'qxwC?cK'@NrFhS"nor}};%b\ʟf~댠M5IyK,cLɎ˾^DsV甈VZ얔l=!rVIEDz(ݧݲ~պū5_w۠/gٿꪠY>mvC*VdlZլF1fq&w*vozN'=2ȒlbWqjڐC؋e8m#>ݙ]9Օ7:b nݻ?RKe&gp@XpͪIwU5c壑/Nl#atnܸclۡo]洌!#d6/& [*?[>|~ܕO`Be, D3?󮵚u lde9Ϳ񱓌OFO;VvϬ@!'ogq^xfsrsQKrJ=sU>pCpmXKP1,9xni5/m=93r0-k+t5KJ6-ݠ&[m7Ip7~,,)waL}>?woDGSr{}$ggY@dlxz][@ɈsYɦM0ss&4:gf_J3s6gչ r|>˼i>+W{gW'zt{~>/Kts`/wʦSj%5dm}L$ ODd~s%r5ETQ 󮏪~O:>t-1ʟ3s2*"D:踢~)8tŔaG \m[X#`iqo==nQ{.KBcIrknZ뎍E MYPp[%%4kꡩu#*E-ݗUYlt>M 6Tph 7ٺݧXW DZdŕuh^׍k5C?aȊ?.Uv#YϒܰdTÇGgNiTѸ`>)7}мU^ؙmU̫PcK֍V<8,xf6ucd $RxbrY|+כ^[,rF2.d~R=B|il|)yn}gZ߳%S0Tqq%(-f椵)6-SA=3>o2Gqaoy1t8^via'<} M2pol40~EꞋ9\Z0H}>u# a LB;ٸD< M{g[T\75R޽18oB=#n"3䛲?mMؔ:|[g~\Sμugk/Se{9DƯ^(Ⱥ| EBU ]`&PUоAa*ޖsN60?뎺cG9Ҕو={죌'P5q|ڬON{|xyկϮZ5Š: 綗凜/[r_Y%+)[.Mn7~"03wVWh^; <˨ioBnċ(!pI~M|N?0k,BXmܘ盬K2kW=y^OsOHNV'i,י|ֽ݊ͫ^8?3֖ԥu՞k>71Psr?؄W?p~vO2N,y2xcD/bӟgὉM,c>ӿjÈ,s6yMV>skLe2f}N+rs^"FjJ/٨Q<,dO \>{,8zͳ/wpq.A;OdzW-tc'_ٟ;.߁'c+Xo+ٿN;kьڹUc^Z: ]|'nHH&{Nݳ?^g۞ZGOo=?I+Q[3o*wnwidYAGqE=ԙDs-[>Jɡbռ*xтr@ԇQ>m@^lQ ,kU )uͶV4+bޙÜf|>|˛p$ůu14\lD۫Ƴ?z^m.6oOX+%ǫ}>4J;>YbwɥՓ>֨sc;I}8qM_?Pܞ7Xw=}~ه4}RViXz/Ϭќ{@CJH쌮@T㿰ctpLyT>9{1-]>7ukOggS[H^K׈͹M["5+}X5h.c{?ld[c:,$KvZ1;/nRE󤹳~6ўțڽ\{-IQK&>(29âlܫ ^Wb@OD>gZ 7{_vQ\VviE3"c. K.dȞ@Ǟpv߀J'O>}'{xz2KƽGbkyt'WMvkY2L^e-9ctw3LWt%ղJE e؅<"ݟR|6ß9o؋3_<;&xlw-ieKTw\>@W> 9>OwGڽ#F_՛\c=7zf5\9V ΤDgָ=iIWi~GkհV -}# fm XP5^ΟDned0hM8>'ޣkYW}r ?ͫ{\Z-lNy-u;QuWxGzRAuqe5vp_oLjx'P9UV`&qXTzIJ~w/XO%k&Ӝx:Yf( {X}tOvop YpK5C %E -+t- .xs>\vO33j}I{La8`Dw`!DbX)=/ᶩ]|Ogi'f?ݷ'6<{\)ϕ0Olh=0gR՜dh+ԅY~ jI py!m+*5&,NDpc ]S^fm~=3=Vc#ε&N=;7F.az+)mY,9_޶8I"n uxtY/ףr9=z j36gۛ.O=EDB֝ϷjL?#aZ˦;1ƇjX{]VYz .x}st%SdUe'^455XʕGN(wsg;!ZG|^>K8VYuQN"yp^7:aBޠm~v%{49p4@3wǕ=ʋ YmqLŴh] )ýJ~i3s{!c\;ߖ\iY53s6| !+;?(>5CsvܑؖӚcI;9F dٍHc\;Y7b9k |o:o_Rxȡ:mxӒ* $]ke,sdg4Ƃ<,6 `)Eih5(KͰ(8DKu_NwFX6< 3Jb]LZ0w-K2|]$۵Z0`1 jΏޞu]×2A'Y52ousK\u7|5!`?7sd~8Q;:ot%ƺC:5mEO{|_Aqn?bq{CXO48r |g&}_K/{ _>窹R%٣qJ:M>fӔpn5x@4KZ ɾ&>zs~{MDޜ&8Ty!|}k{O}*[뫗uN*u/^ǃ֖NMz$ν^vm.{%hs=jW~'-eV&/Eyc YŁ6}=|_f>IRp(\G+[1[,p?,tO p&I 9bX8fP~sG+7<&E>--ʅr6v6iFEnQZ΁7s4ݧ}=CO*P8ﶠɤZg.FS4FyڈCQsZVd.2' Z{ݺYk\wwy=\ `Rcw%%QN>^9vOԯ|t#Ӊ}@(~S3$ϋ NN#ty1l7sjgU'2Yg&c\K>GW^ kV}a3WĞ9μd:]g71o^K+!xl=(H6BZ#qɕsr# pXJt9窲g_ zz+}ZA vK5Y=:40ڧ?=}yS̗ǘ<ʣyXWL)O9̻d/sXVg(|W|&ey:`?,Wo3{?{>NǢ-*z툙i.š2c uscRt>e`@7P%g d[56I폸;Ir_xco}ȽK){5_h딝}_NI幋"pP^*N'ح1(>9?gk;ϝڥ܏rV4h T]\R 'bp.ul=Vead//0݀kCjmG͂guO~ǖy:c6KA[-\j̼YC4~]:~ɢI2GǙ=ioŽȈI=U^!d/{KƄD_v`5QG/ɞo{L4_,OOE<<{ЗϏO߼dѪ<vz>?SaI/i315Ob!@P<ҔN1=wޡ!lk4l%gBM2A+Į\w &mɷ8Usk9we,Ux?K/OggS[ 4;>e1 {ׂ&X `V=1Z37\zM787|ګ*z1XK&u7 |?SONa/x$*{|3osh$7iFlɐV'IKy|7n3vq"=Z'-BēIdXLON*S߯d"ޛhv\f?_05}ehض9::9Ĵ>a(ؼƎ\v;O$ ήxnF܏dH&O2!Ki>7i(5Uop$ _@hW,eb%4\CsyVhGhv FP{$xZOʭ|r[e!DXդ|m^r5c%O:uQ{8%2U,?2jN5C?|YVo7{UË4wIg~^F>9s,r/÷n|1\c~"sOǗ7co{Zn/~TZnzIne}jտ5ln[gcm%gp=Qc]?}3믭y?Y%t.[m&zxRn"p{O:|ZOCLd_Z~[iەypYY> w8Ah ߣb=ueAN,d'w7D@``.+^k4s&KZwn|aON}]!R n`hͳ>y-=Ҿ\׭NOLۤi;I:hIY|2(X3a٨S>ikݛj<ҽEϪ{T˥d} 1&CTߝuvW:wɶ/џ*46yad  RO>y(1lۚu~ߚAf3r Oi:\۹XGr8Udܿ&pV7aHb]16o mzX}7R̖owQ°8Ԏ{_3~y<TcIv_-?WK,:NuY0Yh˹ճ>|ȿ9$]KwC˝&)+h~"6j'ѠCtDcIU8+5DRj5 KPMN<&[`}x9'젏K8t/T6T;J}bȉ傋 ꭥK8]օ$1F7k_z)P=R=79D$i,d.[]\$޾W%L}\NO@SH:қZxFC~']LnۼL!'m+4;뫞V/ ](,=$0뭼.msW0.!F2}BާL zg~o-~>g$[C|}~FO&b~rL>m鳹*Q0*nre&r1*e+hX Kʥe 8:9~m|ۢ g3ֿgyyV'ʎRn*7KsZ,j-Z.<痃>+^xx~Q54\p~3|ؚƂԎb$g^ 9es!g8w3T/ޖ'^O>:HOymؼgz>&Ku1q\zWyu^}'ll*͈c49OHhL4D1ǖ:͏ -0U{زBw|2hKEA rbC]xz hm[ iOB}m.v-n*Wfko:n䉦pv9 RyT6pGMR~R["u%ݳT\jtIf'Pϵ%p[,Idkq&dtzRhso7o˙ۭ>7n $T֥OggS@[J. >k-}v*@\u:1Osk6|fGq;qT#uwak ÔpQΠܐB^zn5jyx9uLu"MP{ G_^st};E trjO}(̢C?>}kCϿ9d,WD{zvkrL}"!6?ciIfdw˯w Le/A5}:y{U~˫K5Bz˸>D9O5Y.}m+5պ ڒmzU7X/27 "= x~ﳛ?\t~2 D3gD |i!NМ,m>؆TljrUW<#ʹĽOhF(q}vw_d-[B!ra6QaoiZ1$Mk iCƅ+M~x:fաϓyc ή5R}߯?oӜ _΂Xm]>`ɓ1.LU:~K:=YsgM>lןq1cea3l&ziU rx:sObk9lz{k? %uW>lvp%mΉ|{G#{漳zʲQRRcu^hBS7^Pߔ^Ek?Z%Έі=Y$qQ$y'/sds#7W-'vH4?FV=?micR4  . ~xu{2/E/BE +=D& /*6vF8`Y{H*6q C&~?VpKii:nzTE@|(<>dyWw2j<ɝ sB`pa3.PآG> 9oaVܫ_>rf==tSWr(w+Ǯȵ^qA&vЛ}.,>龗5O~)Kg9.|OpAܾgw]=1_B{_1ݑ%pߏ!/`ɊSc_3Fs/9kw&svyzX{\%Sw'8c #%>x]vtAr;ʝw]NSOjO#VsFw:Ks#7ywm5u+~ .xnV@,e/qӳiES{AC[͋T@Vmh4|sMK-dS]ŖUT 4;]Ŋlssi~OC1?{K>̟2؟rS4]=v߾h/ZYӂ̑y U][êƩ{DT;B~&"$}ןsoeGb}wr^v_Թ+@ -RNz]fMcXCAxsBjc{F?1Y9[1_+չ6<̊&#|׊ j\p_rܐ6|~zS?\Սy{3<7.Z^񹶀'.#vo#b,ym>z3u}@ЮN+.H@!Xy6P${^\V afE#k{O^r:R?Cn:';$ɳuNy_IڂIdKH vX_oh܎A3.q߆6)_Ǚ!HOǬd%.8c>0D,53WIb{tf=bQSO0FvIedgIs޺|nSjf?&B&B1F>[ ` @}X`( 6 }Z4I3ߥ_Smզٕ.y\}9s?8=ɍ\8|ȧ#BZ})J}Ƚ٧f\<.lɓe﹖/}/`''f43]LX5ߏC.ke|zd.XMe^Í X]⫮{V*ͶzkfoTG,oew7tsabƓ9+M;;m cJL߸2oև}3?CQW3G0ӏK1C0?zj?Inj!!5w78!Qjƭώ\9e kXn?\;sfgyP6yr5uu:N"&iu.]}dTZeca[Wu7tAeKH륎 (LFW6+Y=ҦzxMp_dt_WF\B0-:3bAAON"%R8D Qte'w]ǟP^ 7l;SG5|!B90#]wԒ뢇G:떞OŠ~_< ||6' V)yLNj~5-v>&qIrB2!m]8@oBqsyNʳA{YZ5c <߳k4PsOYmym&7{ٟӿPJA昢m |!Z(oM| Lx=]m=5kD};[|Muu~Q1^||ͮ *ljFz5;IB-=f&^YC&Bܗe!=ێ?>oyPeO+&o29>d KM͹$/:}+5k}-崞SI'ahGO%sHL;}egus̓VlNX˜: ~ =1 F]}J٧\uɥIp)xv63'1eknMc,' pGjYjھ^jYYAҫz^1Űm6D߫፺T@~z<-Ճ+H?A-<fk/|c2拡ߎz߭i\1P6ܝirb#&"0Ҷ/c>/>mn)ԟCz-<M]s((8nmA7@"u_(9- m_g9>y.w^6OLΑ'$7W6 DVcyHIs,zod!Y2ߗ{C9?]NsAV5D?r ὲ.-ky6o[ŋwjKiͱ=G~`+/łؓwnc#q?s?aj LjEKeQO9;ylL>L#IL%ijiIv}ƪ^{XZ=^Wj08j=W ȝZ6sVS1EvU% 1^#Mo#ںx"ڹf7DQp6,YOggS@[Xs >;ZR[O;|xrwi潯WOf+^׸ uGPz٫2,7V:iS= A#k{N߽kr9]iI:~Kh4 4CA'.xkZrL틎q۴@=m#IEߩn8ѻ'xt7XBV߼-=Aad=wi;剨۠'Ց%!5^j&Y5l˸ɰ:^+IEDCn*?o'dFc^āj}~FϾZeZfa,,>@M@Skr8f"#Ġ 0.} @6 9)2{ıv.i\iԿ5A`v-1ץf?ޚ!^M:WfcN-&㽜y{òV1{}b;o ټ&:<՛kn\K`2E̓3OL|ᆚƓ+\&i6BčɌGv|;#g9̭AqsW5Wz\弯v}.2c֣D7;du/{. f}nN_{ԓxF3=uCo(0OqW+mFa_F>2m~95HݓbŶ~>VȘ|uhDnX>XpeB0^w-|ck1n' ~]W{Z+3{dr=6{w+^۬٥gFg;0M4ozV.v8 uF/KP3j5^rUeJ)-kb/[ J76c F94B;h ޻]_n.^ӽ?z\d,[LŨόܿk矝2l;l+kpnjQ>ëҹ$!9FiL'{18f6vYAjrȃ34+j'x.>@xޱԤbnYb Cp߀|jWtJX ^pͶy&nW/A6_r)y1៟U9/g:8rmOzo]_Eݏ+%^Vaz9X.9gka~BDJ!+~=>zϑTuz%pgϚ=@wo]h}"qx)ww}fwvH*?d/Wo=q":&|[Ww_3w([۩&GD7 doZ,]lAлanKʢ=-a/p r^: ȝٿ]wqʼ]I=* /ް!"wy .f!P]FCt2wjs*~CUXܝ^sKN>Iy X ~+{]N!?qF[Yw{1v36uWssLӕp E M M|ܞ<2j4ү ~vyY0:D-^,ݓ}~ő<1Z|lBuP!A{nnseu9f5;64൯yH^󈊊v^} 2٥HR)Y\4*xk `vټ;T Dž$X}V5rdBfދu/Y w~\J^ ? =Vtq"أI3y*Dtvhdbɳ̕'?b~D&wꋿ7 ^~2db&6Sf)&2),X^j9D?*!n82GwUf鵻>b銊eD.aĹB>oOٜ$uzU5Uazc{srn>uGKU%icֳ}ARg~86AHg+N_nAn;vY+;xI[6p6T;ŋ@51,o[$zܶ\osX.A,gzzW ϢA_`s?;&'7Od=n,H&HN~Ii 8t3;vomla΄2Qsݶ}N'ODFL?uq8 0w9JAԴ8> R# ~ЀX7YbkwkN^wSJ ' ՒkjZ: 3өۿYp D+O5tuOf-3kNM~QE>uskm e-Y5^@Sg:'dEFq'u>eUsە3tǐ̔AIgxYN 3eܽÛke?\e#/I>Vߕ[NDǮygip}Fb2:O i16$]#zqA͎5#w뇢a8y=O3ɯ6< e6ܒs+/ _*>)DF͡7Yt^=f1 .۰~&$ȇĭ@A&Dëֻ֜&Vs՛E^H,"A+l:9@BC6v.BAB=x>-/R:FXp=fyc4/wht`*i8KX;FD:.Wt৯B͛_>蛍b+lfz3-jl[gd X 1jq4BciN|G_}&˾OUzoY;s흄Zip_1A˅PR;zøybL4tK=2Kf/Zk+aG5x8yfo?mZ1gÕE^xdk2ٕb{_.%R&>5}e"fT?$7:݌nj*bI?xR5d?`r}gvރIjԈ۝|u&BH}#5$$-o>rW oN?Vn,&1%_ڝuJbX#+l&#( sK-7;&q^pW^[xhNcx?qc.3?Mx:B2n{ 詻<Ǟ:;qD+ӻu0k6ymst?D  {?FzaRݯ[6KzQLE8ğl$ Ňֱ:⎥j~9~T.C<] x;ͮw_q)~S}~t/_7Õݘ}l&O{&w39g=:Q2\k *,h9;AfY8Owƅ̈ >@Aѻ8n&DO6-4^#3yY\ L%"AAG?`OCP3lz:ؘ?I5grj,2ӊ$ߊ\ iXE~OOggS@[ lg )>cYh $8_ vksykx 7;^McW["6 7n$y>qGiM+f~]ˇ3K[ :`Os{'D aA+'m,׬#n*(Ԣa5e^]zf{'? 'R. ǽ}9ưe]5$Kb)&,γsYetk}I}4F.57Ca%=676_t2#r"{sIN?k4pqbJ麬'߽?yho|| t'ם*:k=ܯ  v㞒Ćÿ"siy!2C1NO/7evހV֘Můi4_rA Gs>n>hg9{vIʼn1pr'9'xS|j?HMB j ^A '>G۳?״wS ojcTq'ݤSyZMulњSlZ{7zuV35@' MѕA3z*Ui'e1"Ʈ_Pg>M.=9$`I>ǵt)J~oV4,rx?%OLg>ZpXGC96[M u4"}wNgʲOgTa.=9wD#;31;N9ՋYwG^1٧'#\f)ceyd9wf; Otݸ΋ YgHym br%03񙷚3}ȇ6(3ì &Cjl*5~CęaAmC5S5=xXsó::(Ќ)v{Ĉ}?ܧuh" -GO[ϥzIwHpPH^]z/"DHEsn=Nw^y{W^>Up{FGuub1z8~NG1]j5*ݕVkNp#ܽLw^*:[:E4 #=$=*BQqϓ!/#Kw{*pQ<2lY9Y+ʋox݋kwE_GUĘ/3_,'LSh].٠'wO''6> KR\I%:փ箄{;6u\/V϶ lfa)?GFWT.IDƚ>ѻmB~ݑر[9 !M1Q7_|ƒGf [`^Wm[?S}*+(sab`=>Q)P!]a_}r^@8/]=8ϼ y Z- ^AdL˘G۽ά3"}~=$Ը8A$LysORcD, I fa[T-/JXS>_Rn9}&bpfi 6/uMrb~gsdb8uXԉ:#ӾctX_3;zQ9|e8/E~IkCrG>s?ߙ;eX%'8(Db¥_nYF :Yek.XE5^+n}[s;?ahgCk>;>cR`eK?=Id>9h"^ǎ`Ư̿zp ˝>7\ϱ43Cl܂q<{%^]5xnٶVCxK2. R}9hD&}$8'Y1lv9OwYO䗱U! QҝeW݉b! D匟 Ƀe;mpv;=ErxOf5-4/PAn`'7$H$s=(,fwҚ0W-(+7 vq5Vԋ"bhm'0ߪn$xG[9[|.2A?8ФcZl[&;.t~.}}FG<$R-Ƕ+8!Nwٗ6;׼{'%ce~,U׾tҗwV~?ӹ35;T=VW$83IBU /ɴ[~1Hu$[E6 'Om5\]]=MvgG|GF7)xæhZ78n0W#ͧmiqޗ>]}ݥ0}|_C"a~dsi./|ש"oZT"u_y*G_v]QKo.4+w[gNvͩiY~s2לt6De-1;L; f>s G(6ڧՒ-Oo;A>=Zblt91fW wN˃S IXlۂt%,O_ PSkBZe!_\34u}9:x^4&$)3'?tg_#z/ĦeoN}u= Ԯ=אP }g1;1|9=7?d/CXrux fZr? ۜ&POָkQq;G\2?xD". "/;υ0U5d! e~l &?,g6'|ogGs@#;@أs咄%&8H/+/3/Y)1RbNKn6]9;d$N{Z}C[͡iv1lN7HaVP~iV5_C7~Tg5nZmrKՔ.п RX^ojqU_;'S)[%v'>ĻkW}߱gqrYu͚g:Oϝ$fSZ>?/"wALs o!]):h8#%q1OdHpPO;9g:: 4ri'gNOdC1T_IZ'6c^tu owMk,xhaeLLQ33+8k/y8->Prz)ύ^曰xw3pS\qDtԄOggS@([    mUˆ?ZE|ϡ"}9_-[xn|~>GxFu \SO>@KRI"gȘ/>θn^0H{_@ s{=6Z;4rRs?8ө\/1=NC^v8 uG>ׯݎ$Pܴ[r B#9?u{o%Ǝ6g1K>%0e?RMs{alL=Y$vr=4 ,_mp[-( }&FѼ^-s &9v?={zRpIH֗S7uzoz>l%M}CvS;S7oG?xv3vGEbQ7m󢼍-Ѥ_5nuvة|uke,y}ϼ/]JF6Qyn_޷bs9/OϠ^~ʧc]w5~(?)1:ќK,%1_bN}NҢ=)t/֎,bC#qrQ"g3V!H\6꫔_:˅r-k}nrۆhme4SQorgbY<]K*oF=h{niDe5? S7Ι1hԚTA󥑙KX5S >w(JJuܝ;>ծKF ?KI;ȕ?sٽG֛) xkizSj 'Y$dߣ wնh=¯U[ ZX=q买tdnmUT4 %f\Znќk`~mhe#{(1߰#=voN#׸\#rv_z=sy#G}j>_iA=+|\Zŷ:{IltPO6"::_Pb^=ri?7vƚTi%>W=W"'oM'5DwQ kL>1g$ O|Hu_%MܧO4fuL I'N7-*9($MɅzR剈/- Ie4n9y [sYI_&j]i h 8Wg4e<] `s׽t$b: &8ӏM1$yB~&F~%r!Ϫf_WQl[눣"wQu6e2.t`q*oiLnytIb51\qT ulrYab %wǽ<.`eoy9q>a3VX&+zkGg#C*Cc;z'_+NЗ+\.tDLLW٭>WӼ!o;S~.|11 L&|t]rT?z9{![iaL7r%5ΌR+KNF8>2}ҷf }x4[J7ZӖ9$rS5r g F}T2^'\^KZVk,?8r1lGʳq~O@]๮%+#3͹ӭyl_qU|IOgi5V jTʺ*/9扮<8BTίهQ럗Y2>'w]P 9m?6ҹG˱X7k-?ٸ(I|m]"nqsyA_sBoǢ=Kt_cߙ*slEŸ|MkqWL26}eYxuJC׈^=fm"c%K=/O.;-P3S3G{/{GfQ6,z6\C~`focߗ8r̿Y|{?;ɹwY5yWwިZ֣&~eN3SX$<LZ{Y2G)%m$6r4I=nF9 mwisUb>Uִ3P+?;i <VSM7`#}OѸ.=F4Wq(jϫPʹp0Ʈ R}Q+rW|>ѻ ͹rKߟ2Q Xd1ѾD-%e<<9_6Qz?yL,J@$v߲);K)_3CߧsUgotOQ/g籹a(Λe_.{e>2M*B_ɕOͧ:8!94ƒOy]0֓rb^?tR.wى3}=!bPD}$dZX_aoi]2;/f@R8{ֿsrcy W4H|EZ%7 ]>sJO;!ck͏l Mh@&:$K^N >m|el?{Ę٭cч>7wo5s3bzvF]bJR&/sQ㝖/t/߾7&ΡG{isvu;roU4%gۯ|H=䳄<$| ߽o#s%,'}_lRKs>2&Y{gqX~y2g-Zccl~܃}B?p1LK~ΡB2m]:!U>MJiև[:|Ǿ;Cv>tyXsd~C,}w,/R_Q\,>qi˞dg?ٙX>mi1=%\ q2>TUWgc&w7h1?s񿱍ğNv9gùY|zk5AJ6cM;> 9|F=иދʎf vFИZxIxRͶس2kESR ~M Qo-p_O =>n1ݚs.d;* ]pR&n.U:vسeF8Iog8>oR0y)vwDiXs/82'[_r:er/cqxsǰw'sߋsJf_EAK&tW;M F7F9O}ۼjbflMpLlk5z<6WN}y?mjMSe循Ǿ6!\m,?H&{=l:sv:mbwjvf}D=~wZvk7٫Z>UbD7D9q._G\G'EtNkd/zG^ud׻:k [5k40zg:"|]Ū+eOggS@H[  ~]@xy@#{1g꿿oI}g ܺj&] sYZZ?< Qqګ~lwZ's͝a3'/QupV9Z,붣cO?sΕ{ {9־`NJ9jxwإy}i> UXCm9?2< oɇoJoPe9(ׇoHiR^#s"=D;\7w^¼쟺?gk4.d 0ޜkGg9D^SIOc8 Wt=\{׬z4 U*@h $ >K x1F(OzDʝk+Opp_/gL1 &jW=%I8Ю^]AZGہKMpUS EC?4HO&Am["cvC`~fZ;BM&|W'~ ֹc?Zk6'">*C_ç.klǙդǏ4yHOc+?ّg>@场zw;~^3|_7Ʈ <,Kmb-4#j^j=-[vmb⋼mJJ璋sx+S>kK7J @x""۞??u~qy >j5i`T%QL BkIzӨC ծGj> _{EGp6|텶 v'{7?Ӎ K AM3bjEG|noW켟ݺ>=׶bpYꥤuMJ}X>{ꂏ>|Kr!A<}dF$⹟Xs^c[sϝ3<Ȑc5#~]zDE<d^gJ烅%:#KkO|o5("yj'm&e9n9~@==x[MSy@[թ fwObo+ *HdX5~y>+{?W2>gsTgS7w/{kw5#5Ts*$u՜)T4wL- e4їsfc?{\gZ+?cXPk`hϹu&֛cOi|[(o[&?.qj y<|x(٥s {e( o¸`卪TԮڼQ1`O \FnQ>ճa$?XozεY6]̒-zv/eCRI04'wkg=@N(#?K{<\UAeWl4<^{KLvq/a>bޝ9Yo{Зj|UE\A\Ž`e ħ3xTE E޵R[ 1h99=x'$CpƓZ!a(v<\^ J%4_@> bKJ4=D[.=bmC~xb?rr ]o^^t{;7e$f03IpՇ9~wxb#k x8Og ̗I҆8 9`la>y;]*]5V&fkx6?GG{O"_~a:wQネz~l|Gzòd }}hwe5qb q_ds'<9p?s|s3P|_XRښkr”u6_=r k;L={4u,^Y{XZҏf.Y1;K_}_Rĝgl,sh-%+;xZW~y4*H/ޘݺK.[O ^u(p̑(&pf02 z3ٵbzϴ]gW }[/Ķ- r.)^钸,1Lx#::d̝^Z+Yn:{ ?t{u{U$b"Wi=KvwC҈7gvaJ* Tۑɡ -%.t}>_V[#{\:N;y{.dxW] >-Cl,tCD~;Ev?b?)7<-r ,g7.&"_XdpO>syJ.?ò㤛Fҏ]3aj}rzt6u{Fϋ.\,js0f*|ei fc]_~^Aߵ?əe/=|sKmxo{'Tn=_续Ew//)1{~}' YنL5I ex/ѨҰ?>ۛ5mcʁ%D @d:HaOCUyayMSZf=h 5 (KFa9X(M%qFk/@P{}{yٽ9cI>wkj$Ie$ }uAI=c[ZnB0}fԺqj{b\_abv絞Y1VFS[OA:>qXnU]ԋ^~_GrmĶQn?gK~5s=W5qObc"~CJĠeRχ+s<g%q8 (cJv[A%qyd 0s/٧yZ6ʏQU*xdj" ^)%G4Kd.&xᜰyBk;HǤ:Hm.jnd *WƯ5}L:j yuufyh+ӐMi3D~N7&߲wrU.p+ 6ny=o%P/[3G~ϵFq>4&>{}KKM3D K*E|knЅSj^mfI0lP?y, M$Ulӳ'S. Y ۞O.l{ɺeKu'yEJ~{K֋ٜ{+.UJgD7N#i&$0G'[e_uf`_ 5=V$א|Z5G-ȵ |<菫.[/Ji)!&sHWP4zOǧ.{&>9[(_nvw`[e?3s5rMU_ LM@ n<[smM9iCL݄f!FCJA%6<{Zy{Q8_Fx*l$qnCI>WkR٣_Bj0۞_; OggS@h[ Yo(~] 7{{Em=oSF6w3~ϡV y/ x^McWOB rX^ûu#ڸoUa{=1 q7g{yb: o=.,x՘+LG+/YsfZrX[W)gzhx˰YZ=(fN-=ӒoھE;VkA]`ߎM.n"bci䯘]^wjSv2G}}דYb&rrڢlO(^`Y ;ynEϽ'Ý_X_h~HT1ܯ"Ο'&$w$ 9i?'g{؝CDvUi>+ت83\8&xAN355-OИH$|ʍG5 3A3cDv[#ywSL7 Zr\h uo6㵔't2V _Nx+&2.߼Yz ^=];~,i~ nY3Z=V7 dn]'΍Qd~٧tݕMb;PK> Q|[EyˋU剬,wyK~5EjZnlzB}>{IӑjRm€'m.F|/u}2Zϗ3}C >H:+XyМ ]"*/lY95|'y!/-0>985:!ҿl\鷥^dhki>;OA+O?{CUy4]r-j'ZCKִΩ3gx[܇9 $s|3o MXo[_,+9wwcܛ|oiznjKU$|5kbKcTy|T-gZOa3웬ԕõֺj&ٽ*ޥO'1ߞpyjV^ZVo:xL_Fi=Og~34ѯN36Y{k^6D=9dyX6Fu$Gtn:˓c7=?`](:Dom%xax샜/Mel~ʚMwDv?}x"v~r񻁎΂iַ}{?7J>l)uRK֗ic̈ޮMFRmIR6}|`G!V@4em{ n^~xs2"g?ybk:׸>,ork.Mt Wkl d>n O8g5'3Ǭ\uΉ_<úv9w_q-_'V})311F}Lru㼞u?mzGdh[}Yt5Nܸ.zq^g 6]l3w6ɡc:sG)9R3tNF,Eԛu[}H˱w۞Iu^囥3Q[FKfz3g0I oquw9Dde9r/pSRo$?ꆝ\#;g,s#nWՇ:L?WCP!WP{{66GmS$du48UW~yۯkvkݹ%mBbǩ񀀅9p0dd_vgfw_}>7v`X&c mXq}8Xu4sqvr~6GG0Q'(A}b;Zүia B! t2'd4 7{O\fvLY.9kW,ek\|R}SRЅMzC>R;6|2{ۼ ĕV^v.mq~uDt_)a6D5:}X)X~ΓqXx+I'm|fT*gH`_GL>YMo:LW6Xr(?/?Mj[S}fN*v [i6_V.)Qk1yU۸ޗtoMyp 核6q]s=zӤS?hEwZ֍I4P.UR*L Sw`u޲y ij0z5lvsLit8su%qQ,w{=U/I|YwOW_{ {cb ۬NF_ݜz=g0f8x6c3;kf2g^VC<$H!unM\/sMzU=̛k5 yw]^qN)Q{6=L_3{=D\2#n/\/ 4_J=Wb߬^+jJ/}6x'1pUOQY_qӌ~zs;5魾e2?9͇ݿوTOtǣ)%.Vճ[1wj+9ƛP6.ZG Թ\`d\-ȟfdܧ mzW )eb)Ǹ#b !()foMh64 (Tϒ?s(9+{A7ޤ1}q~[ i<2nzM<f}l@<jD 9qf9]}ߞ\9VFOSoL5TT93 mn7v0\6yrwTz}ʹio ݠ/q1uv1}&u[`D &`S&k\M|&Lfvv| dۀNYsgn/%~N 2\R>uG'KɎֿKfg_xrB<|_,t.qc|a7td&븨{4}Ԟ!ĒYOFu\C8}%;Cow+1;6Rt-*RV+?QgW*cCOηKd_`/t\gvD_]L~dü#vojGs;]%@Pΐ[Ϩtw^[Digv62fbr=Fc ] `ٍ nkDLܯoIH7 g>c-mQ󺧴^TUR1N$$W٪kh%8^J:\ FL_3xڟsR?>}D~O;g@~5OXKQ:`ZLq٣{ܥrj2 C|^?i{&&}q~Uyӗ,߿|ոpSƜHmvu}7yDW|sw5{\Bs9GD`c zzI`{#ӹ_ tmO/s=#=ÙGsJ>)c!L|Ϲ[v7I ס"h*SLNNdݤs32^ZgLLӟ쌜;lAFj9~gךlΙFf{SaC> Lp{V,mRӯ>\嬛zOggS@[   MA$;z>& @,A>6V;h½ktk:m]]U%I2dz7ni{]T"σ +t}uifœ8KdܾEwgj +R5Gys)ll`w)_e(;ZCL߹0>fr͏;oGy<@[ޝ|(%xy9P]uZ$^/|[.>C׶!s3iMuD%%[ܷL&"^ˆ~'IՕ>}AzVdS|~D43ʘ7Ol-zJef8X˭զͿ{N,3\^\ o~U!#WŁ?<[8c$cô,-NvGܛ&nMn.iww]{zU~B$V~TV  #>A$`ͻNSC|3`oRu{g;aj (pQK[ƒԚDtI ?5ڛoA&-OƏfglK坝x.Ǭz!4O&o}s0*ҿܯ\fks|(rxQs%21!ϔ-v߻I2'vx~i~חŋW, =LaWJ])/ZAgAs *0U(Ǔ- v/'˞_?=;iLZ"*zd s~~8狼s͓ٚ`ϏN;=\sg*33R~zc<גZ\EP}A44K& 't*  Ng W-Yͳ~ q|17p&ojyM;6&.HhdlgFq-MR >o񱕹X.ޭ+?n<-OTpܜvq8ΎiV1_a,oprf|88>U_ ^3H?2{ z]$֠)k\wlqe/sNЧru-Xyho-HbIA_u8i2Y~??fR?Da)x_ە#PbI=;]+Kt-#ln v-y 6%@AnS1";{"ỳX?$%aVxx}XrS)Wpֻ%s*\.~ E o[p`a<w+~1 mԠ`l]8W'f"`i9~BYd.rPSz g4f|zTw cyu&l[~/o}`o޳66|:GaI^L{K:wk/kKgjx_xq{Uj#w*.'{S9y%~ےͻw/O\7Zqf|߿g_rYXd 7$wbט_9;s'GX.M \M?O-.oVXKpLc|#&&Zai9뛙蓦wcLMyqw3{P:Ef0$mxo&bw LȧNp6U 7Z}5?ozҗ,h$T')^UNn852ORViux92wR$ M<fkxLnS ²tZSL8Kf1L_p`!Cɷ*j}?iODzwiK~NGuX4n]sT>6٦P?{D+ޗEt &/V˾6}s=';˯yÛ(s fDߛ%/:σD^&Z]KRE˺"]\sY歼3A.?=1l8 13s%۩3>>3sFn:(26v< |hcyzc wu9O*]92 hLQ4_+:5ב(50i w7_6(GmYs] a tшtTSA| /Rߕ>jx}@] ۊny=}=}4wkb7>,voTs\p^$pN~t  HgB(X,pxZj["䈏 jg߹u'_?k0М49-0>]1z/EoXN̗,tN3bؙ&(2UkyJe>^Z|T5(DqŲxy K`!4sl9%=llk[mT3XVQ4>*\ RZ^;KV_M}}A|=_D@1;{k7N9X{, b2'~>=i9[cpOߪO]Vصd`H`~`kJ+/9jT|╂r}G| ,x#wh_r:sWcXekǰ \:^5)KMb|;km6`8&)7wsNΉ!^1cpIw`ЛC 29s7OT Kh[6 gvoat+Z#*z}6v$->Zŋ ^%I5SQ{VƆ3 od4O}φƜɧ|%};\+{ǴmdW֤s=SDOgyOr>6+gzGKܩپΈSi\%E^*ʤw ̂ڲ{;in lzCiu/8? '`VlX+9ěwM9sͪb 2elDo;;;DZVC5$G"Iڣ{tr;H%C,:uk]pn 4 ofLi.n'bڛ˛:qqbfzZpGѿ_wq#V⣥>}Գ}T/X/{so>/%,\,DӺӡr ӿ8։7)90f8{Fѿ=XϏzO+yizX;7G}5e8G@?9oyVaѝy{pNJMg{3لHQkENvwgHIࠄ%XTMqqz˜n7FzWlo(c >;ӜKMS{6%x|B Z^xO@١OggS@[~0[ Z->^[|O-eQ]X4}3kygEKLu{lorcW"#_wUG$75b[;mjܕٴS!N&MېkKus}[CXѻ\|g_$9'kݤtχ*hZ{?d^L?ז !`ҥ9..Ot/{##rde ~t5۶2#=KLsG]_燭A]b>. +vlK9_,31␧7Ò .y֢S~v_ X<$"V:[;N7J=xe.o;}}RwRq5X C/2cQ4-r=oR悐”y%b88sϭ&"e Ò6u[ #ouޞ^rMFseY;{t +ZJ|67Ϟsfkx }#6X3_BfF!2 v 7 SawMZ^n4Q=4wT w)J^]p>{kRF]fzԪ?^?obx(Оu{cj X UR.?7) M72MXf2ʚ|K_:|fDE cdk m -i-ZmИ_pݸyfv#džF=y]<]_ɮu>qwa<7Dz/ؖz+l?.-~\>'ynP`mύRҿQ8#ܑ^/=!Z 7Wm7f/3Fod+M!Y}I,ޙk1}w{M^>H}|{K5}L=Ÿ[٣Q:W l \>+= UZ[ z3IГDk-l/nF.IKb௵c:>O[۽às93pɱo)g iG[:n޺nyyϲ]g';v?{].izJy-[a>7,䧤g[lǂ? ̨.bxm{bK=KeiO";~ߋ/ l?}=yxv/ 8_䜒 ?tdrOt&\;SKOG_5#~co=0~=c;.w@fVd毟g|EXz\zo;lZ3Oŧi|fӛSKdL5} Y䙼%_5li+m2N"F{ G*/ `SW9( /bRmRLhVШNw֣c Sk2GA]Z-qZGy{l1V<2o痮o l֔{Xh z*oL, X$Lžɤ_;iaovcb4gua¡m{/dy(%%5dlϩe(}ѿ (uϔ's&&2wxk4k 1S g'~ i骵VU~8Dʻj(b~I5å)V(gN|%߷+O T*K{7K[w=Լ)v6{&EGz zqov^T==IœgjZiE`~BWzY6ڛ<(}݋Ǧ&'#_i+<';vn?mONlxRyK~m1NM).>T%#7+/l>hR2~9w<{;գ%k|sFzdOfYb kD9?登i|8dհG|sZ #w0reg'*S{ RS,uh&U*BbTX 'Gc4dLO89> =9@"TF[ןx8߆/nEsuD䛍jwxT&w?Pi>`Z1=4%[%ˡ/to]/ kuy\/$^74LcFOa>?>;Bhg7cm^>o[̜x~tM~m$90 ֓DG,OArIՂ0J=9`?3wx+v@O4F`>o #x}*E-cd/<τ] :8~Iؘ #UP<-eH R*Vqc( _'IZK9vN܀8|s=;1-,9S"5*Lzd:ٿvR6w;^kf}{"D =Ř.9<{$]۵>;zB咛=̯v0](Nw}'qIE0H!BZ>s)em ~ǷY,G( sPjdӚLz}Q~,;Vzq9ˉ;$y)r- +;{f[s?"HS pn} ʠ;<kRܑ$BcSqn 7i.ԄD%EIQiƇ OZhQn,\~T_ﮟeO2)_jk "S +:k8F:@wX.\W~VڔC5X~r}\.Cdh]$^h3}Ύq;OO;EJĠ~D?}u yA.?їޠ't^mUhy-ˆwâk_Job/'D2l^O·?iYw!ctywwa0~=d8sLY6%Ctޟ;&GmnKI{Cg+R}4羮 tMx Gk}>䍤i͡13⃜ abԍud8\ܗZV%r>3 <5OggS@[d[y$r m'b{3 Kl#(2ٗ/s܌;=R+yom=13/@ >r 9vKy~əMgvO@}Uɼ`M{/> A8Y`AiފS,22'D h,ZOojd7eMddg{Ip}!ٹ5 J@oM ece2 ̀=Of ؛UVQopUmGw=4"ESv{i?ܿ+,N}Ӭ2F#)lP{PsFWq6# vQ HgwF">x뗭/Z_65N4 0jOAmn}Iϟt'bjysM[dι)hȦ4hKr߇.dWyb_xr4lk<]Kxc"Ogs핺Wi׼ %?u_%vτyDˏ…>gb>@Ki1cXvzޣ]y4y>*R“~~Є cfe+MmrEQ˕͙~3< [nctOYgmXv;{vm@ oO}DPtiEۼփPS64 l|,m{6ͪBH$WA_’$Th.ߞdM5i-eJ)⟥8q+u''$ iեzbK@?1/ǽ\?ϭ\1ck25+,K<vi+긲HJ )ӒOWv:$I;Fƕlny668C#۬S7ONGCjYʍV׸_3M2|I(x3O/~^'DrЦ?=E dc# (cD\br'PdJ~ǝ%XN~<ɪVYlNA=ġdld?Ѷt8ߗ}=U ٙ?so@yӛ l(|2'lתǃGW+ATqevi|v޿ȰU8npN"NǾBělu<6`We@Ql:IDd~Y=*&_Ӯ{.^R1۩ř]69sy9e2EDx7Rlڬ%6S@u Fc5=6Αlf ǡ{1MA*QK,O>&& $^=}#?KRp.*xZsryg:\AN+\ϭc}HTj7o?,__U pm)b@R%z.Z2R}şn^,xvl󩡱6{v=y=vqVaH`WCIWt[ߝtu&։_~uiJlj1$}}ӝq;0}^s信A_wR,¥$[(I3Ud"fĠzD1d6:ɏ}F,68kNs|q`.i}r2c\ydo }x&oQe'p7f\xk':#x|䁪fHP O{(9?>j(f?qe`CҒW3NH3Бu=>ler[@wF 41ǚ9ۛ-伤L;%Y9_ja6sd$6jZWIV g><bk) |e@"4ƎM<vs8|e<ߕ|Ǚ;7LK\r# uE Xqp˧g.p_{}p-/Q̞U3K ~I7S}v1 {, g.\ߖD5Pn~:l]%= z@?.dΔ鿌[^,-K7+RrQ`'&l̠ (;h@Q=0YTW{-z5O8|clʳL2Ul<rIP|Ѐ7 qz+pۭA v]Aˑ#fGVOsw|D0MvU* 'b9rR˺ u}gG ꟜX)/ {sift]g{F=Y' C}[fqv޺u;\q7xwB_@f#3l̾~\Ozݦ{L|6|w*Jo#>c=3_ְw)2:gܽlIfhۈ\3kF rb|N? a4+&MGE%v*fҪ@ Ҫ,8)wַio}r0?%;>iI{qGt.H7cG qrP-c$P9OggS@[9Y   Xo+=ScH)yN7&ݬa};7GT9-fF@VųRc@D^wM[ԫڋxzb˗)NL^7dԵo]9GBov 6 x;gy8o.i3'/R,mx"/qo/Al~|2=Wnfvm>҅wǡ=6ٗII-v߻w1e>L=`hn_1GbΫcZE. ͟=Mφֳ/ч1Xyvo<&6KIu eD%fM\= ؜91Fm; n?&}iQx3| K^<^rGMA%OR^#^`Rrk~^j6//K{Ncx+ѤgxrkvBM)=U$aے|>ObFe<[]U~u8]$k:T1Vz%EЀQԝVwq']3, 68d~qy_]=ߌd<\HuKK>< 'dL!;2y~ҏZK?^kn󈑖=aqhrg}SKyŇ߳ޟx=Ѵq;|j}*yu?ٺ}rG~07raO}:/6=^~N>Po.Ol{"ټEtNtw7=yov9lPSWh _o>+b?-+mI9-ԗu׳$'*?ծߟwu"ࡳ|?3:pH ;~,ءNg<O׏!4s #graz% /K'9ЧybT^c<X@k%eM%;#/ʺd=Ψ߰Ed0T w:DsvnvF$9i`c@*e A U<^i'dw>juyQ]cqsaS:5Ig)씇n T 1H}&(myu@U=O]T%H8(mS]wY)u-[Vy6^_ qx'  iߦ0 mkqom[qN\sv}E3M^\l31[4dϟ_Ddo>j~(\ao~™>yTE_]$j=,b[t_ܱZ5LdHwA(c %tKD*ya.lKt좟Lߕ}uq]We-֡E2X|gkԓ}'ka偛}Ld;{e6џ\7,nonC#ǖgw`fwĝ1{Yaϲ0ȻK+"d7۞Z~O-&? Y,6 Y~~!85oeo->]hEކ܃W9ɹ ?2x,w;6ֵ %wv;`Am{V 8t:Y7{4]Stl[;K),w*e6Ol$vj-(QUeM~2(2>͚q++w=܀/LYKS.I2z-7}pSysx::WmvMo={'\X),c4Ԥ>=s6v~W>Mny̸ >ucٱ?ҺkFX\.Utz=keSd=ԛ"c3+ XȢ66f2M҉ߧL5(lTUj4Jfi}VU^;YV*4kMkv* 7{mcO޷CHȜfJy_ƙ}Azho(̯ BԻXYrn: ,3e9smEV6>""Lզ\Xrz.}'O}@L=PP>_xD9z߳qzƸUCAM26=qaK" k ކt-.IYq)^nu?٦?/і0XnDBfkmHWc5Kˬ/{VQOrn//=r{Y\Y/֯QȘ+-1\Crf'\XS~uis ;g [G9z~Ms7VZ=&ܲTww^6i;)=ݺhr7HC_./?ʴehaU_~2k+"_ $voGpK '9O}׈5w?tW+wV}uWU+hEL,=<]ewwA]A]gyN>?{/ݥ]?ߏүfv:ZuV ;?ʼ~CL^R2v]9>8gZg^{Dk[5kkwk f_ AwL_T4{_YiToVl&^b\tTԬG#s4vvt4q?Ax}EIvH>~[#GqC]$.ŌRd3'4ٯ^j;3>Ͳ>;'16CS. |:hgGw8^Jkfx&~?loN7)Sfڽ;矝3%(=wr]x$:k\:^v/ן=65{=~t&H3O}~7 qXٜ'1HTDXts C7 =`?_+= ʹ>{(Ĝ]}1WH1>K!6zCdPAF`bVN~Y9G $SS^e}$?8YSRؿZOjv ^6`hyg6%G]> 2bRʌ R6,ypܷkEni f7`:P+UI e'~V }Bx.hG_}xL9n>麲svo*2n5+7l@ էYӬZ>}T_jwwz,Ԓ>:kYJW3KyIw-kx2ţ3di,;|Ѻ/AZܹ_~~70d/dw /nir1e0Rj^?K㚡$M{-oA{SX_*fˆ}^򶉝+]6m GgQvΆ7x!䙔qwo1ʭ2յuKtwco Q:~o5 1EVT(H{8?٩+su:ӬlT$ ]b Zi4@lϖ#58sŮB#3~<~U('WP7[x{Gn1>{w<>߿x6[~`jq9svG׈v>e9o[;jϱCqy/A_)s}%'b̹|*U.ywXW)4:w_{UC7*cX}0wvd -7ywF~sɜP2q(Xϕ˥R"üd,Ӡ&\MsgM}4r~.g{ݭI=~cZ׽m(OX=N8}9\^$1N3ҪjvWxsNI~Otgo)5 uN&#W:>:-@TFiǎeϟ<şknk]psYj1e&,ΰcסh+6qsNZ 7wzwMUyznYNp|p7rQ>v1ϑ ƬZ{s/}RpO_U{9cZȏ:/iگ|)O/};3q,^x_ξ|KCv"oco"so|6J 9羜 ߚߵ Y>5Z5? &UH&W.@Ɇ DĨ>8d΃'Cf`7T$қVV}X=w?j <ݤ?ad&W*kKH,N-ϩyw굠jx3 T m%ۛǨwI^פ{dx[Y1xz*kՆnRR) V)SOggS)[ s'ɔ4xj ݉Vm}Zxkw 5`{QzIbXaਜ਼<4dj%W;T4dn1Rsw`oq{eePlnXo/NW^ٿNy>c:miy=\=_ }xsj1}9K_ޕtfjM5?ӹ(0׹j}?:x<c_ŕr1s^Iʹ0W9SYZf$}v+i+OO:ߗ\.>6fwlk[}L[ʺd| *i6PwZh[Qv 8>{'Mߑ]|}X}+Mhcyc:T8V1 P9U__(rb 5"E8qvvޟm-9=ڈ^ۘ^l>2xhoCX{&ӦYy;<wW}YF/w ]}:*_Q*0‰Kf»&\zF<;Ӯ'v{E:%W|.:.{^ym "Y`cɧ 6\v-,erN?:H5IB50fga_@G v56e_1b{Mx~N槆{^V]YJ&7TfMf/ySkUD(DS>3,V =Ъ7j.!ŷJ遺54yTm_mv /|CXMcb;8xn6PMU8 [H+}8F^[y-Mz[n0q.)n]+}_i[r386zk Ek4kh\qUj5xošhOׇsY=6k1➛U3y{+"Z\nRCvu>sadӇ̕s,QWػSaXkZ1 lM-3oɀl rpO?~DO=oNшmLf|%&y!U2D݂q9[u?tk 󌥢eeհا6򹞌u鐌<ˁlm~ }XTv6 ת.,=UjOig`'-zfG-jo]]@'ޢ?@6< zKyr.Ktk˒::3k?<\"dp-=}ܟ[,e2LU|k#3MEI'}s^(mX)耟rɭMX"8¤ilTV>?&OQA1,^HId˘lß1<7KjȴF(W^z<^k) f1'H~V{?F~s=t+sX׈b?͗F}m|'o]u:m%#2f?є;=+| e3~] {gwތ_;>wg=g5ڜ=J/Z!;jN}~c&\k’K>"s"B~X4'܎#ѢyD :>3w wOktֳ|ztmKژ)@[*z?o˜g]]cl&d'![;l sbqz8|LZh( -${ M fUYwPy޺-bS+?|J!10r )p`]UYR.a@mv=<rD%UXeچRķ[`g=k4|+􉸽/N?hpO8*䦶mȎ: ز~]eR1l6qxf ,92cOk52lvŘgYL]L_ c _NȼϙkO@{N;j77.*#V|eH,wMOh,p{>iGŮjl *f~&Ss~Llm@ҟȢҩ=(sl"g!B$F{ -iِ|*%b~M@_' XxoKxc3n Ymu  幹JUXF@QS$e]沙](iő\Qk=>ĵ a, F#h0Lj{vuל<$ v&%K8nôyl7Diϥ_dwPכ:wXR$ j}Ρf%9+Ȳo|5ār^3`_k4ʧ';"wۥɩùjs7}ߏͳvD{ .<(7ϝSMi7ϼo?` R–H k A[-8Lnԅ#w,nOzkݳ^k c+7z9^RK&bM5lG%Ӳb=oQ}ǰk6 ^Zv"C~?=]_>ov.;ާ>ų=u0,zp\/~wjnbFsʹw-#۷YV=п9+gx\LɈ{~I)q Ewd;JƸNC^b|~3ϥⰒ+>L= y +yt8OH$bKVϻuyR$6f\M*#xg&Q'TRvOҿ9 (Q+!"n}=M9gZcN&CA?@"6=%O]kִ_jOggSI[s nvi`1hO )w߷ό'?ΞMôGo))X4N:?ݰG+B&f_Pø^5\1Zw,daFQ#'ضi AT}ΑMiڋ9<-]yng¹+ޏS8K3=B}l#wyUyWS6^3Jȅ(؞3>#8-2v K钿d K.Ɍ>,Fk9t>~6FyWE2OBFKC>=k>a5e|e/EOg=%ϛ^7] AE\:8gzJ?}ba[%] ?~h%J=?4 ñ_hsR٣)פ3=baoΞ7\MK]qOcȗ= `(_E#"A6CZNȤoִֆ}J ,2^KN|]ۖbmfMy31[KOVV/ z}D<:P]\aHH~e+reg`*/Bw8כJ1SE}E b^kV Ju;̳EX_ x*y% , ys=݋{k? 3~׺<FROxKjXuXd%%VcHyNk~8ԺOĻ=Xz-zη_?P|O>^\̷_۱.~"˰lajsDKFlr&{<:x1W][3][= a-% C*"b>0w4[0Pt]2eCS1;eE8}5ͼdViukΦNvྐ_^5rsfJޚqr"`EqU??uMk(;Ms|1XL$+iΑ؆>6w1ʎ=ߞrޏP/Ţ{HoiM+!)eQ /9Zn2YpoC@`hPYwtOZ$3Mcvu0r.W|dۃY_9nr/y>Җz8kAKex%}xJ/w/[[@jYfEv7S\gᐬo:2>Rd`Ϧ_ln>,H/1gRAwyԩgyNFlp.Zr#̝j9~d7޴h֯Scr vYذ+7Dyԇs~|kJi)uC*X}( qәof.Y3rkfC=f\1pU(y((̛46bCl/(p8|Z* g"r!RcZ~\^34jZbki.ծJ،MДۗ._ifG]bT+N-0߫-scuvixm"d"M?X~39}q#uE|W.ϹDЩ1܋,o 0G_zxDqwTZ`^,!7(ў~CwI>zkn ?řXb[$f/+`iy@6i ݯa4ƦK2Om &<3sJΛ!=OࢴGfNeKiٗݙ '2`>,0?-5@}-5_KΏ6}؃Ndy!loI=r4z{Gr%ݴK5*RG>~Rŷx@׷}~={4y廏{4׈ve.UJ ˥Wy}i`4K\&=[G7=uS+k !V:AVOlQۨ5;jh(6+ 'd$Eļ<$߬DrW"W$?˒){vc%)Jj|y\xSGx:']dEwɋzE27tV?=]U{Iwnw'N}"Qb)LwN1 qap<YtGEegNm~ w нN+3^A׭9 f@.>}?G=|IO[rXyɞ:>YZ|'>ݱ#Ζ[҉P>k6/#>@l~_Y@5@hwĹ~קyוkmpMTo5WUIq"@#z[=>{~C^}. ۿ6ޑOӭ<\ׯҐTw!xr{j>rvv9W.صL<'?G·yO6c]R:g _z9k8nB~o?sL>4?Xf9b0;oEC̝D os-A,7fi8> zk ,y ӧ?>vG-"$K<~g8E.g-m>kFaۏ^{Fδ1+k bs˂sHcq6$8% 7 ZefSs2/cW}yviEğGwb $rYS=tenܳjΌ>Aˆ,=A»E߹}{Y!r{D,>0O^MJ%Ib" +nup}ּ́9w߻LC>(Z9.ƺF=fрǝ(^#ֈ> lT]{K.;uwǿ;F^΃v]pe `R~+{0\ΞO&͝֏`zRD~.;m^'2clʯt.Xk>Aƭܛ}o6}~.ƚħL<)΅_eX%y+R<ۓ2[9q|*@h^:Cfo)XY_ϧު%>fA(QzNѧc, \iv:UH+<ѨmlJo aVM/mjݷr/y>Odeh}:ζ} eqI2kkX0M|vE ͅy.y˓u.uzbcg2]~y.}.ػe?']f(X!(9~9DphOl/,oGTgi7M1ِR.ssc{p^ui o7yrۚJɧ7ssi^* X]_4qⰜ h&8؟EM H|&l|֩$1$~J2Ge+ <jqKK2ΩwE/V Hp{2k%pOggSi[ :  [c૭NS #fEK߳&Ҽ[hY~3kw#FFnk k ;y=M^ם=.D6q1Ƚ؄R"~}`m~RR|͢ʏ3o}jG_Z'G׋{c".ّXG_5GOwqdqK>QX-;7ޚF7z:s3ٖY5f\fhywfgol- ^"y_M_=5ݛd!fTPxA,X'dg::(fgKɱydYUMGv]ZcJts0?}ؖIgtI_Lsq|`.{)h0?$z|Nv)E5%-9ޜ<>-|f7IAt6:8oO5gy5zdœmѬj?7]Eow<SHu$co mLmŃ`Q|O]ؾikͿ_xK7eY#K'.pU΅cR TbY\7_dB i'[ "b11'q9^>6"zcs}R[9߬'lMӞ,vs{?\$II6Ng}m&y4krR}^.5҅s>ٝSKy:*N_eK?J6+Ӈ?f=qARSڌ7UeF0=M>؏sŖU6K~5NY&LϠ=NŏӶs{y5T6E)zyGj*g~'r{ۊKDizGԲ(m 6q>>?ۚ+a~~mN' T=ꓩQ5[xP"x}$dGSN͹*9M^K{|_, 4vĒc㼙ut9W^-w 4'L0Z#S|*+a~8x8"}G*b#4Wj__pއjf?NM^jSJ/_e$BCa3v9>8Űyߣ6^~ ^nL@,My# OZuHoU>|~u'Д|q,'F dbx;#R2Hl&n*o#y:ؼ~0oS|v.؞IrN7g7A,kҽ 3}Xkq,2{~Je]J|##$A-vE!=碕-ۻe1^*oU/VCzwyc,gwc&ɋڭR՞ݺt9ػnWn4wyvTՓ$#-69[kSpa*[·7ZkC-ׯmr|S&W%ɇE/޲oqYz~VyYxndžGhޯ2G#j%eŽkO(̟/pX-Rݵ+:C}dt݊w}[gN>sM9Kܙ̽3z*:&dYl$t}vMlCݹc" \c bEch9A]trGT5V'dl{OCdҥe#ΥGmX.Ur sh:WҲpϨwq已x?PXUCsiq.WnDLac,󧅁 h{ɓLͼq-^uV~T'֨<{uCP M5%bc4x.zhԯ[{[鵛c7Zf7PSP.I%Xb F`}>W7B;yx|5ۋϴ J n=a&>ޭ1IQF]d:}YZR{g;}5Z^n&sxQR5q:39ߺoqn Ѭ#5D}gϻO)sdY^3/3y s*2ϛ=ZEM}~]~e]}Cy,sr2?~fW5 ;2wF.>tuY#z~=_Ȱ$dý/tFKzi~sjrtB6EǟI:_3럺Q:N͹/Q>:L9KUZ.,`m"9Ku 0^_;/{P'saOǐ#/dX%6yy&?saIgm; OA㧋eYKR֠w ٽ8L\~tE`'2˼yi?yOz,?ϯP<ͦtczV|QQ_evCu_oMziW9"`*ʥv҆|QIv3|o-?Ο?K'_5Ϻuҷx5q{wZ 49ƖE3숷Jnq̆>]\OmX[lv给wΧsmz^r ';e86ؽ>3XW*Reӻ ]2wdb1˺Rk9}O9&ɾ~dff2ϋ[qWw[!&ōZY=qCh?~Y5C1ǒf=ɜɜ3[GHM Ζ'ct|󏛁V,lw'Ѧ@tXz6ƿs{1S./Qw=K/Nh}D~ ԼgR~8tz<͠!IJ _j0 -G0s_=uEM7T}+k+/(&MVy.1*j[P=TsͫxWJn߆R'*}q.u^|L7VXAU[,Abx΃Q~zq#s6˫սʬcb>}^~}2+^G!LIϗgmǘvҫy咆yC>vDY/=n)sixg}6g|lMw6ia}uZyN Q|v0yŻ=ayݻ#+zm5ϱ$\tY%5Zl-nYbIz u}$gx 2m':>|?.r==-Eu=xm贼N, S/A=c8w)D%!}>|n{*?|  =`{\_|bMݳ/˔nMc`橻s%v\& H"9F4U%8o^XOՇC>?ڛYQzWh<_~3yj;e.kL̗ZOsVR@IXݗ1N>/N} ȉ.fvϟoKfsK@/pi>XqHq9yȱ'tA|6rvn7CW x s̹|".\7*uKD9) 6(lb}Y1}Oٓ ` ifDZGB_j2mGԣZc:4)UřXA̽춅M|E p쉐E{y+h~1gz˘qKO^sdݹsUyyP!juj4Orz9?e_}>\93K;D YNFD=˭x.?4ݜYR;gm!VdC~+ùf zDR.ZLm63_ Zũ>Z ζl@`Ǐ@>Il?6`\./DĬ&95߇v=P Nor%{1|VIZ/ɪZ-)[ϑּЧuslc=v)|EuUI#\HWcW-H%GY=ָ=tKnnlNn׀9 8n? l(8@\_wf]:+'/i{M͚,{֟\yw3r\".N孇jq?5IK$\C _|.?,6 o0oR!,p9J6inLyɄ;13)!OP_ u9sבas9/=PZhxxl%RiLf~ySGQπhLϷ)Ob4YXU6lhfP\>w:Z~jy(;C}"[,}IW4E'\@xT ̳<.߰r!T19xU>@oTf_Ivi5'ϛrg.!cE|Ygu[ãE*7o/ $a2*eCd oZ@,ehiĚ%T{,cUfHSEkg6V>}}~,'>"nchNzTKR.f&&U*r֟:sܚ5ʬ1hA|[0L2Rϣ?ֿycv/Tu^@_;}py̑v~?mk~fG}mglgE^=rLPz5L'~:iGc-ZêevVWJmyÏ(>[]yV s3J_W(49%]ADEs%:$[)5i}ƶliQvSm?}ۇ dH9 M>>TXr@f5>eeL}?Yo^v3Ϟy^UvW.pH@לb^E+F1G_8 asF*EcurhM몝|\>Mէ0#[ojx-'M_c]ܓ'xy!q} 3s8\cgrɀWx>{˾4bO۹gpUlT/zV^R`YJ0WRT^^5~mU?g?FFH{7mkZ=ƙi:NE1 oul_3g!c6oN6Btx/UߺkH{GU0d7sIof Lm #xC.Mz-#omߒƑ?NHgajc~CxæAtҷZZ3񥘪<;eX8OggS[ej:`GG%04߿':s^=pXۊ>rn`iꪮT u.g_9]?^4ɗ%n=O@9+oZh(uou?㖎lIr3~yrS3.|X&Iud9t堩+6.fq9=˲bso}K҇t\Ǽ?c^Nl' 7]|E*}ȍb(cA"Os5bg=?`5m^lc8:ٵXvd7U\Tڍpcc ifuU맿n& PvPtuw;/&.H"`)Z=߹1/o =Gz~n%T;<>|ZZgueQx0.kY>k' YҌoot,.C:'=0̗xo7Z0s󱼌O}_?>'χ[o} ϓ`68,c?ȓK_ /޻9קgN%_&:KMEq{'Wjw? 0煡\殌wX-B:]ɭ, ;W82%oުKYg)xi29OLIċ˜\h/f9pz8=,XڐߠE40oOLm)hO\\\*p;`zumBGItxKZ .)$f(5統 hL|^{ӸL^)lfg@c:lZ!e{.˻^71YCG,]Fom2Ȅљxs.yPYdOٟĜCwo;Zs}5 _wc5sNdf?Y{r~W6w%/˒/[S=u7;4Lf}d> 5SCi#SJ~~~e .Jp> ѭ3{]+$ufw{>b>- x0=΅Z.ƜHi B p446.e_7Gw`ֹ^ X\COOL`MP-H&Ϧsk/ Wk.*/I8iWVSfǜs 3;dٽ;(ÛlZp ^c}iA-0ӓn}л{Cʹqw"|t]y˅]Gf6/We_K|OTBgge}sBe9Q"3}Xޭլ,mk߉$'v/3/,s檭?)f\z*9ҟ![,+R'9].xץg';vh;6lP7AFt&s. I8m1,a*ňÕ7tF1˸ZPވP&?7 @MϾ&G1LK.K-]a?iuDn{/ZbLFK[ hǟꥪVفUS\ Z->zԚ\A"nD?A%`0Iަٟn/Impի`詫+K%`5ٿrf)c[sٽ/k_{^K*} Ά3>>m#'fdȬ6 ͢A0;|2᣽͐Ǔan#Ɗw'W%5} Mr}D_r5몛 /=I'Ybv'⺏GWtu"?)>{kgC˺ƺV/ޛr7k+>ɴt|Jy+ M̎+CYqYŽw{fw=\gT-{"t3xY^64Nѐ޽D!q7%\.g}b.%%l,k::.Umbs|#swkE9w$ݯTfcԀM-(){^Yeb:jh_K޺;}Y4/K<ƞH*_Tj+/O~nRJ-.{]1D>fEs׺,3a3o<0ݧ#^&QZ lym~;,l,?Q-͟źV{_?\Y Ot*I_5C{6 hTky=ʅfq%.ϯC'\`Η.@wZܠ،t?ýď9~$?2m_rau337M|Ve:D`ZNTݙ$" 7S4<ȋ -f9TtY[Q{0kW{N7a=[E-FS";N9|_/]&kFru uAn2jC|p{WQ]>K>1\=E]&d/D~^wNNw:"d}u+QQ\ztu\Fɘ$q;io=e`88Fy!띗z緸$?sùjo~?cњyn@jqܼglWI.frl2ˉ̘?F~:{dwet̾Y^MޝR($w^.7?[8TU )jTRR[Uh^ј1`?aF6$E N D\>Cw,=@=ߪA OggS[ m|zk? fn: i|pns5@wM.3 \2 ݷ(- @ [_2ZX\>~wW- v5qͬ4Ŧ&EhA\gyrǎ;ZΥۥo3"e}v^z/Lgkd~9I3]N=BhE޽_DvGķIK.'P9?{^<~=k9{>~;5g枻"==Ti^/=F/=+2ϵ[_ﻐz59Msۛߗ'$OVױ 338әd?ӝs8yrk@Q4$9IO3=_D3m~lQau1-vσ{OJYsvKPr7vxg)dOW?ɚS`$Ξ|OrnV8;\mr>>G+Iy`֧cҥ&Ec>qk;3uh.s1vmAqNf ;@N3KTm_*/NW7ۥAkmx^\c4ݶu-9&,hXr:εv;genÿesq\w>zDKnB" COa7;!xɎZfk\}umga_.s_}{ʉƶʓ}k$ծ>՝:=؈|ۘr,/ZN:pݲ;#ޯ}jOTp=oiikzb"΃C3矜azy.uvVJGz{T_z}yi]r%:&9@6D]5sԡK"3e \IOM>WweUUye^B񞞍K $jl?띃 . |}:a7Ev/;KDH?w}%5;.u?_zLЁj{'HiF-T^.:'[Ӿ'o:X杺PPw.b Ҩ(̈>$ >:Fdqɬ_Iħ?p Z8?z' ͋},ZԓWT'3;RYq_Rs\HTt/mm6p;9< I&Ed--}E@9=szg-e7x>yRqΊ@ {\5Vg4e;ޱg7Pvbxrd"R<"/-5{*sѻyڙkce 52TĠ4ɋ|ڙoHԼyRߋgOX]\/6*ͮweCүJ~~Gif,=˛,yWb6L~Hu}Ɔ}D*zpӵt̩sm8{kQޣy1E69!BJod3,\74_1P<tPe# 0szc4=?s{'UviGa.jؐ}|=)eW+drz(u&ٺܞ옟(S Ml|z-q|ג7 >m)Al5Fr{55cJ Ms 0m# 's"/br<¥ۙ}wԭ 0e{\I-=\_O} cz"s">uW]\q(;\Wı2y6`+֍t*6W.wjqt6^w:Q@cL00c9^2KL_^Zph.J|Ԫ;I<-ϵG=!%gɦ%iK;HKD]62l;YҶiܟh ).wάe ι/F6}_I#\-;Y1۱Tg5-B3*K~VJ'IWRaьNXYkiɗOS^Svڳ_oAa0[f#>>\o,]v/딳cB7f['͚̀ZV[*{~q1޹YnÍ `M is%1iTұ> >_!AC&5zs}|.Worsg}#p0.GCY&T|#10`"3Z1:|uVn^=qswr\.s8AF~#赉jn~v>I ԛ~0eEu.-kꍦNTIH4ФO^}9lӴwM uǹT*f&5V8EfWđ=vC*T7}4vM$r7WZ,IlEDpf'a&8/jץ{X+޾~:tszanwP[0`6; \O~b:bD\;@&Kȳ._BYB(|O#߇e\:nr'_'ʉ?`Ϝ'9ds49fJ~i1 gOwv9':(o&>pއ ^&a ς\i#j =* ֺ`fdjǫǜf+^3Ҁy;#{%yUn>_`{8~d~ME0;G @۷q3/zvirI\' Ƞ7ZjOu#:Uǵy8ՊqO_mdԸL>2hTk))z 7?Mb97':wG'>캶ϼc)^Wf9Ժ1ϥVKq8<7K\o6ފ}ztgԱ!ߛ"qtz֝$r??sOkJvށ` ڈ} OHpBp\6u_SP_w6o j:hMkM&3;RNf78{E^ϟw[a˿vz]. IdM|yL1U<fj:RaV\1?2SJuo ,[S lD8@(5P@ފ|Z*`$?!e+E<1{?|cǫw;ЌLMr8nyp @\‘Y$8~>wl>TJ^u + >"{^vCC ~T.-$/>Mqgg7A ʾMfϧIG !2?u:>OdZǡb\+~պMR}u~~5e83MNX7'iһX݊R}E&O[pM&KHt[m> jF.e-/̊ttJe{I.'.0~햀4;/L 4Svvcy٬l- ^KJ+Y~wnoǚР=Ν[9?g=gwnxXfr9 rY9P{Tgz+ȩ( OggS[l L556S vRvM m~=ƖWa^w{@TqUIKI"dE_鿘?muL.]*ڙ5}m_\빃xKC3Ⅼf_Fh[ÌۄK{(3kjrʿ޳\-;y_;Ƞ~(tBw"||=kA=*;N'v̲)d$kl4zИ;M1_eN3D!"#/!s}f2y7>}(d{Wt.>^w[0ltwux>W&k^8 gQ>݃IjS3/ 9et?cyo%Nr5+7yZj u7 `*Υ8aYIqV*,AmzR Mh(LQܱMu#_HsudڇT:p3=\W/׬?,-W |oΔ(_ _l_f5O+CX6"4fs:sc}Dyr@|e.di2?6w|źx.ݱ%U?/Il{Ix=v%=epNh%{ 779n~E/xM7D0|7dts᪘f#5k sjwi2$ ބۣM!0]ls[O5FkI4k<5*A1?ja@/vK$&ݶ* |X˚ӤSܛm.~1Kt2Jڼ=xlU)KV}NV vU?#l!NKxWi4N'G4;Boh齘5n9g@!_^NG1imr.~O˾kЇ͵<ǹ+2 Pw?<1ן黗kI89JF~'n#(6&g礎f\ޙH&3AV5UOVi]&&-|t4l;qHqcs; ֟[u`s%\;.r"y;Ǣ'jG;)c/];ZPPxvwd]0%}MZ<arN]yݙ=v{k~Vod\]AQt?yl]v3ΏyVp 侲RqGӋFZD2^OFG!f_gӭew舨j<ʞ #|Naϊ4[&mM-*kY@4WqXoa)tGJR @*e 0V{.5ysw7>c{;ִ.USM ƪt,d5]G9bBKʷʀ岟M4>plf綾==7spR(g>C lQzQ[#/^ݾ=/ĥm5_"wK'=&PziJ c!~{eg?M[c?syi>BzA\2e]1zS1_wN(>|I/}a/eb\;r]qTN/ 'o2]v ׄnrRY:5.P j+b8Z =/٣j 9Xq}wd^1uI{]蹾4ݕ;k$7I(AZE2ZWu{+7ZunX[\!_?|vVڌIsvt<F_&u`|"Φ]˟YAa@ Lj[3_ZNJ|ϝ2ב6[x?*ne3{6< د"lfM߹>WcaMs<ݴTU$aqnʀ.Ɖ+>99m]T_@{_.^.ޢ}Dh.=a l<\}8zR,u"- e*&d3o9u=p6'"6is_ֶ ~ݐgίg 'w1а>&ֱ77z*{~"/bpPàutO%\[j+.lbRLuU7abJl%޺-A nk/|Z!1Ѥ;ngS\}k7>:P;n@ut8av-͋{zsx Xƍ_yozPz'lzZ\i#/Hn ߨ?d3(,d f 7X`2~W?ed.*+zuY欓{';SЮrx@Wzq ~ }"T+?v>e֓q@Dh:̯ٗyyG^͜0o&ic\`zNyw6_Pȴ'v_)>_ ߩ~bюąs.uF=a'gtʻ֓pbLay,x7w9ߘHd@,{Z?Kqg&=b[b}k}bcSn0{<@-*CF+&|!WBhѦy6Ҡ4U?{gwR>Rd,Ā:Ϳ˓ǵʱVS|0ogf%QבUݔ\^蝅_Gack_ċ˲7%Ľxe 8{e~{gy_ &eDʜҗSO|ķf^94]U/K}KQ9p#VK>Z>k?.=6{e;Gݶ_ |0{n!*a(Lkwg.y_ZVe=Ë,gzv5Dm9lWғ)m6+ ;MnMDC%I:SSW!yqgupK?9;?jZ߷_(#9oY^q0? ܫ60qU\J@OggS[O N>Hh==?'y=26\F87uOn* ";|ח{ 4HL4 ;]a_fG,k#21:t܈.hphx{bMDX^}uǒ'^:y=4$w^ϿN]&{ +g8XMozF7W[);OZ;ښ4S7e_0|U{z{8S|WX-%N奕. <3 S Z/Sӡ%c3,ɬ>׿w[sM&,FطOCLk RoL"}Nt;a`(Si+ly/;I8=a+#s+DY7iÃj3Ȁ y:E=BRU"p_ufc hXZi_zd ުmjc/|DOTcu9j{՝%IL$+cc3f` V YĜ*GݯV ڢVP:Kz>t9&kݵDħVK\,7V>iswL9y,XU9 ;S֨ʗz]Eslzɽ3vD_0=:k )MxOe3.h溼Z+ݑ] M|K&|psiޅ:qβrxG|d62 ~Z2i{/ɽt;3`gwgwM=g|z?>-#~󯙥o$cy<:Ӡm[vv[ߛxLd54yȗcH /MTƿ7Fj&â!G-Jkt6d(k甼$kg>[bj-^-䮷X_zuش3z=‡چ :&WS$I)F| oE%bb\O>$21o}oӇm}fyds9!v}T!0Y۞ E̔_G f;$_669Vⲳg&*k#~!Aa\X%{=j;d_Ͻ'* M#>R>z'^3kb]}N '2{Pթ~'fr;EfU\C|q9Lh{}2"|mւ{|V+(⼞}/oڍ7d~S-ço) f+hDZMD4g8=垺W JTMYC5dpzAU{c 5f^gA_x=cwjrIc4uPx\;ܘ*1\Ib"PGSa6]B]㨳ح62l܋ioW_E{N?Βd7*wIm;;D&&#j䷖!=ޑ =5zW˲yW Z{kIR,z-eγ'4aYOs ^?ڴf-mVwJc|n+!z@qZ/|~SZC-{AUTgw_n>ƪSRDQ2c3q=?ˡ&G4 ,&cO1ii5rSwSŗ\"<!#ugWΓ? fVE ww5Iyh>i?} z$4:q}K{k|G _d(;s+=N`P_e4 䢴8b;@ $me/ks, zȞcgz uO}X{\pxSm|'78VYLpYHjfl[wvgF~`νǛ^{Ww|xV/m\(BQ ]h5V1bQ G,Ž{&:j/ ٠?klj.U^EwGs,"w|=>WZ/=mO*J@[s;'/K:=ӏIܻulKKg{Į?I<ː!zq9lz"/2w}r"~ݦJ.7]+?ًG\ȗ&Rmkn=mI:@].22V w1_0ٞcN7-շ+TPtֳTa:nF\-;mY*SZomskQ%i>aod8|X(^zx.Sx{TfdFXfǭiz~h_dGZa\u.1G7{g\xЇX5I-u9~x-O\oA7?ycŽ"Mly=}o W5Ҳ֏Bs9n(s{k)wگw'531~1i%%b.1]>i}ewQDNyl~cF$ř93 OK!Ud?Ò> tnCwfV>XIQ%w:#5^Ş:Uho-Aq}P"lȏJrY~z|V ''eٟ15=sV߽!7&{50MRq [\E@򢠯;V6,;hO M8/w{Z^?^]b+7KvD6[k2NO]O7etǩIj(>=|g:ͺfNn>cK<~i?0;4EN>ߟed?MNeL7@-0f)]2d}Ys =7=?Ğ='Vj}c{;Ӎ2k[4O9s|bCn/N/&?ܳgG&3=2f})=>t 7frEMF7KpO=z'K44I1V8ː$4O, Anlꌶ7=ʧ`qX&o]w\\b 'f"jĩ՝S>%Bm<o=N=GOuA_Xcsmn6Ϸ}ZXu4M?~of|,eZ{o-r^}_mL\X2\-}߽iY:Ħt$#s+}X4 5 G Xwo.(WwD@Y{)wӓ92O,L_BI&6"r;t\bn}? ^vN~ml[s~||Sm3Z,n|bOmo1@K 10OOdg÷T3UO~}ֶ$~unϤhVnʷ52e*C@1Ocm|I,ƔVQ+0߄%1_<ԢIʕٻ;{]>jmjM|Q)#& ax4|t}ز[V.x=uUJ\66ybG}4VrE_'кbyc0LV{-t`O|}:O:@B}PB~oţmix̝cWeFluN1?_xCOB0ٚ%թ͗|[{m OA |Ӹ=bZ4 })+4@1%dl8W!OۜV~&7H 撧XO+W0oFCMBǼDP5ȟp_t^d(!E+co c-2TP~ y13xvqεc4pw2c{=SǞ&3@ABέDZ_ɗv:"E }S3NԼЪ^'@tqܷo<'9;oآiƆpnfX1OXE.%۳>ͻe':'j~aFT2Ͻ,K|rKq0"Ľ>箹PD(Kfl^QO|:(/욧׭e:,}`^k·o2M^{%9r& eIir.8# &?4Z^^d֣rDP~ږœ^SQzuNΟׇdX-)IQ0 zc-P&9?iAsT rd졦׭ewzcONQ . uxI,h+J0Tz`hգ}>iu{K L6rU>*j%I :GWRvz[olQswDў<GSMWRlj#@/6M=sc۰eG;AZ;"9s\߃,սE'&">}и{98[ r~ӟL;Ø䉭f=.YǢ \_; vo}O͗~{?$,I]gK_Wm}}{_*FWY˘snϞyWSӡ??X9ٱ2'Gz2mf 㳩y 'GvfSQd:!a~{?=+ܬ0I/^җ/{)fI%I@c&:w s6j /I,lQp .e @{ut=ߕxT0Ug\WWTx*o[ ̌[TCRs09Pv>M3=,R?c&3oqm_D.21νu## eN=XM/Kf=k'{s]oMG3|qSwJ]:Y%3%GȣA_\ځȏ=VH]GCë1lߗ "4?KKw?,;9qb(ފEڹt᪨`kGSN< yަ/FF~h=>lx3= 5ok^iˠ;!t R_ODg KO(CPGCp;՜}fاAcumj.P$]ũIR;|9HvD13kԿ8.FAi{r.bI$Eiw#8EBK\vi]7`.$~uE֌@-ZU~ӝ}[9ssO?>?*ֽV6/JsX 7฿?Yʳ*֢$Ul |=N{-˜BѺ}c{?0S7 SuT,IGt2zyQհrWlZ\^O׫_vӹ|J6lilֆHڎe3̏c8A7o LZRάwثT/"?rn>/<%e3?=f3пso曊CC,ӌ2cqL3"d)G?J^1gZRUerO3:Wg kSV- 55{:uɒyt\]C̽<9, ۙ(L톶hG,$M#L,d~Qxջ6}*gY,liR&̅YCq޴#;6dO M 8e1$gy< qa(75&#zj.$d5p']ӏ7cZs.^8^>?ܻyneG,_|_KNk-OpDFۂnniJ1g{ވQ%>NwAr5W4KRye<0>(}:>g}KeoY;s"?2IßxC/z,O'?#:NO~6J%G+X'}y' jl:ϐ?zgzg oma# LkO;h?_҇>yMn',3C;[k^GHr+I{$ м*[[2q|` OggSU[Eۋ7  ~iq^ "oc|3z1f4Kߋ'vyzV !3}.Td9:9ѺOg.~olO@{̱nvKa 7'$޻qVª a]p_ωd:O ,?Ϲ<{1M?@ wh|_ˏToNĽ\M-ROrx >ιC_{~$^C{+E;_W fæ%%^r=Ն59k<"{\Q~Uմ3lW/}#Ix[%!v .{}6p:}koS~VS\zn8=D'AT5;;dE}>&̰VgP0dY;nis\t1[wokdUzVa6.e@*>{kE#^"|ϑVQV|F?oF>?=45h}f .,4uWw^$I8^?-1vhGDw ;1>.uɍwogSHӍG&ך:ﻀh󋱼^>(XWat1:Wr{,TXX]U[eYIWq/;yފ}$$ax_1QN?RA!D>; 7dƹ_Xcg? 1r_,'瞝HV2u ]mCe> z6r#gq(՞Ƽ a;^y90yȧ!+aSk8 V{q\C/ :bnə҈YscxŎ "b3^aGo9=ϝ/zFHE#:pPԞ:..,F/қ*BM`,7i}jXJ+ Bou_NjK~#N樝|*8gRc4u.G 4%(ؾ|Ux˧/Iy ]:sOy)-2wi\dtS#ztlX~e_'ݦ3[>[Q.["~i2{z˕g R$ŁwyUN^.?Ͷ0k3hͳxȈdR{uF0)5|5zv=(}.XYi½\&$Dcރ6g/?ojɆ<z䇤ſB>J.ie-jƨr=:OJD+Tm+l٪+~3[ݗzE+%;*l4x!l10_kenek VMƞUUŘ$H|`.8~R'}{c.g_KӚߏTbVɤ0hHkSq%_2_rNa޸">C7Zb(xyG}Α a1 s Tt..13yy.:|dܤTt}:.O"WszeXüe.dWP_G~&lO'^G6W6eρot?9i=i~Bmȶ3/rɪVwF(;d691d>r G6!._[s/P[x ̥ :L^lmηˍNq{LNFaf*kĠ~o#Oչ4|B~)~U+f$!/E:Xck?~ٻ>ϱz >0*]%6%۽\WGz|qLclO&R>7΢]_7$r. ݜjAj34\u`Y9( kuoV%;ʞ|j0\,~ԇy}t6K0C^Ζ>G:mO.ߋ:p׷4Q+vCļVE^o?Wz%Z_w3YO ӗ6듶;/=f:o0o.dd,?7?^rEOUuhF3E2uzo1ɜUcedcIR}O~M`G?ir r#HKSF'B>LG nr8i{Xwu4{fKbg!s>B/O e{Xʝk~I}/+j "z,H^+x@P^fP|[cy~{OڱճWϮ4>\nZIRUf>';4<sjf]q~Z$=請?=kQhNWvMܬG=< Qkh+m~֗ק8k#!1ޤ_/}7+6뻏}]tƢy'Uϫ1nd#ki}}y2tms9L;џ܇3mqh:f첕KO {{^9- ebiٔE|=昻\G=? k[xj}#:.X.jPZ@MlZ篸G+ܹ,o;X^umHڹDoj)vrz6jD7sWzM 'lwIɖ򴍒gSz7Fn0nh2l>RY~> f @xXA_>GKs_>ijcuTa`m {mIۻߓ8w7wv~<R5F6ڹZH9\u4Pf<o<Eǯ5>\3`~?{'+{>,JҺ!>>Ř{_Ž+#@Z]==m\P|ڛ{6K'n*cՍ匲8VGwF{xb⟵ewb}| F_ց9jOXb=K<ϖ{=lesu POvy&6]ͥ &ЁkɾE˼/7@m;þ돦N9皧.GD?Rwng~0#qa>hԐ:=η}!"={m7j-k+a-7e"G;W@Ykr8s=ڗCXztm|Ϛ:nјHof0SӾL 8p=d3A(@s%7|syPRmAnj>@P")zo{Ɵ!4}`n7uonJ'I"`5*=_j< nhw5rޒ@_?3]^{^Xz!I{IZ5o?]UuleQh~)xz*q7o ε[7i%9M~\l%-}Cܓmm!lzsY9m-?S8noDQ/4vl5؜'$5ۜ/GbnX%\uO6d>Bx^?l"`s4+[Z~'yzϱ~b3- `T$a4*ןoT\?qr&½ɅJhwE8sOxSMh y3T\?Ǒ#Z,A-bA6Iu[.̜E!=! wkJlm);W)w_v)=.|Wg_dpwU>NBi,wd|O_OAVJ2Z^^%u:]9g~LQhPOmQǥs{{.y6s2d~=aƽK(<%ΞnhS=þ5*[R8;( >B1==аIpxq棶VK mcRI|g)0 {+wT)?dΞ3tSnΥkXc>}'IVO? |̢7r̾q ˳N49O}mr2=iW=eɂe6Wb/n瞿s]k!7Z؞{sWο_>0>gq6=WZMn윆ːmGے봝il?Us4_"+sAB.Cnآ6dujmѳS'S;^l1efFL%PZZ()ͨ[66RiܕZuݙe>=;$رSul羟4mJɃ(g4sy^%;ϏYbS`y1=Bd.i{U~þl[O^ !\q:;ZE39t%QoѶsk/ʱS/ehdS"go("y.sX5{YcmMoJKܗ.cN_kQt^Hc\烋+fzn~Y:| TC.kUZe0KԯШ%_JW 폞 9'OדoĘ.x٫'rߺsNfݯЯh&?lc҈ΕIeLA6<5S9fw#mZ ^Pf8!Yj*1ߋ ,j@HU "`VK57j٬1޷ cf̚~BM2F .uP‘Stl1;`A6l)%j^]C f _sYAt<OWvL5Do0]6C`vi\RI Eޕ3^>npZx躂 ׫]_6|A3Nhۍ}: b^ډΜQuB?ci8@,:ݓWxWgѴUѭ>,vtF7̅cHefF|<(MڿGGw!ە֪\үd_Je.k_HS^rɷG!JZdQO{O./+$7K'S,ɺ'0~p;D ~o',{r )u+"nx+`e8.}s&c"gGc.E_jPO|Ǔ̹&riڥlnKNAsǷgkLMwlOV@]<|i 9$}o40b50!o{ 7{~ սyi|맵VwuoبA4M]ιbK$SGV,iunNE}|n<ޛiժvs&hzog^I{d?=Z'՝sH7tmwsM|^6zVYl CYW~1B?BAs(+3|+K踦5b~mV%sy9gw.d:q $:{>b jS_253·=*sxl4M3m')[#idn\7[mEqʌ]1"|Z-gB Dƿɞ sgKɋHӜa7@6!ܼSe@}nv -x Ǚx%ܛ}{Sk;bdIDyI*o[o341_zqI zm 6cU 6(HbD&)v"zQӄ9DbFNH_#w^LW[ub'>{EQ? ztɡRe?K{i"Ɵ MoqOXxv6CW6a[e`{"Н=K׹3?p-v-&8'}}2IyrXC~vfO1rcgfNZlKaSt,O޳L\@ >}fBp}v~}Nٿ|_klOd0} hJKGhRfx;zΉdWrvU+eb.X^9r|C? .k݇ݕ^ ;c[L(rK\-<ǭr,OggS[e^[|jm,@MP](Z{cO3붊}41"W>L `6u.Wm1[$ y?-Yi֟J|ci!ﻭ;|{& h9qŶl1N4w~xRH!"qB̶zw\0e+Ͻ\ωlqUtCg>cxy%sJu /Ǿػǒ'>9s}dgoB,i_.s7䨁^䧾c#yWeع!{{..5EKpFNV'/u̲8}}d͖nlydH"e'>?[~{ Z䝖rx8W=oA%7,no3@ܡMĻ)ɒ%OCe#>~IcsaYwoe3nHU^ҺP(uu)w.u)'e-l$wkjr[t~;d@襂kuB.}@v,kHV{h #E֙uG*S1?~+gR0ukXO3Fu1i/ɵWMaF!7pW M\ma8 تvtKz[==~IZ*e*n} 0՟n˳.w}'ќa!vzyۥ*D"箯iV3{<]a.K/{ǝh}FB2O7vZ_qڜ-TY9fDce&KWo+|K_\b{wtq!Y{oIxt^q̜c}ϱwT76i~a~w>:GTBRx@,{,,{K;}z:;sz^žOG} ?ٽryfC,}ʥ$Ŏ0/8?y]0#.t+6VEԙۜvsO̒bpjmt 99NξsھOv'KurFQK;j,_R ::z:NAZٟ/z>f3K^bƚS.≜'?X#[^tڇ*(~M>9U<ϳ?3&zL^9yiGEIRQ7D^>ý:]׬7* E-o#L;3[;F:&"NMqۤrt?طgvzIwQل<Tw{vK_/P)+1<ݝς_>}%W2]牐ܡyIBe4!Os-n:N+oE7K{W_Τ){G|oNN[tc\wھΒ,x.~Y+/td@L{Jıt|,qb&ӧPCPLr?U>E/ܙ Xw` L?|~Ss.2BjVOD+Xj+- 7L 4  [ N%^뷦.#?xeG_k_}?k7G >=m}UQ9lEF"'4W̙!^[qoP 0WS ̥}g/CgS9{{r[G\V!Fh]a{ PU Eeu_*9O!bk+:sݿZTr^+(μSɨg ɯzOr%#,oO_㞦EМ>\M CDF$#&,L7YKm?<»"MDHv<j+-3Z\3}#2g,o3-ٍIfXw=yZ۠Hugg&e:{$ ⋿X'Khʏv;pӖ"ýꍢ;&SD~0F^Z}UϞ1 )zX]7Нr]J NCa {<^h?: g׍Q} lІOx˵-Ztw`:n')_]]p3|.WW> _~7sa.8vk ,gWXKJr~o9d䟯\.yrzھ\F&.(tv&}宓]?gM-jc/ik{vͮ#/],.jK{{yGt&'f>[^H3mm2md߀L&C[H={>J>~ȆX{F~&햏0[AR&!=,i;P` ;^6l1=A!g;I^Pkߜ߭ȹb7Kp3g>ZRg^D # {܎Y*J!IEUŖan_wϘ_Ęͧ˗o&Ʋ˱"hqi^Ogkh Sq5ݵF)Hܯʛe>ƿ픻w6D\D~>bWw1&,z_J. `YuV&˥9[gU|W>o{8C^86εKN6h.S"(y~&;k>F:I!I,1o͹&y}bXo~v+?'yc)I}~:Ƃ.a>RX}JGX$+Nܟqagfft}w3gSg N2eyN}.^{Yr)(CwB,*2֜\m|v{j?ϱC 1v_wF(S{T=MX"vX=ʊl=R˒b~K3kյ;Ү׷[2aCEMQ'q6RkK6uSC]CZ[5"4 '>$7>oc([LG5;,e@iJWc n/ޝy~/.s+#.=*3~ep5,]K:@B5/ ?u6Y9KMs9K.{,>Wλ(I7S&-={g%4eCIB ~5&b>5O\E}HM@V0ٿ;'/ηuerOFM8iky);Om4Ie SnfS:kFL%Gz|o.QnX4yk9ITqN&>`=;px jMGP"$w5AݛGv1FdxNث˧yMcf=vRk\4~'7a&ɂ:ބYi'4{iB}uMjͲ~O7=X{oKAۛ6eDsu8C͟i})sܻG'. c=yAˮ/WihfEiC'Wa)uGC#x"g_ᵨ,}13 6_:y%ϼf崹~Vr҆3fy 9Kp ,XOU@Xl.k֋;a6w.K)i}jlΛ,~Fpnab$s5;/[EfFsrQZ@[b|)z|Nħ9cӯKF]N"iFgXchJ5x$ְNw5ŗjm`@:>[ 0dOggS[m ~ud3/b 5TE¸t+a~EZ=ʱأdba^2Z%qH\!!mo[q5cFXEqj#hg&-psylg 6#ՎG*Bu}xl6m^2\4^ό́3| }x2|'5]q/ןY9_>g&^S F]Vޮ_[[U <"l>#/}:)]ma,FY{/5,5Q{xM-SAFs&2blUo+o?­fwd$+IKҫ497|džCGi9˂*dcfFҚDžo6_`lvM m),x"4&ɶ!dk}?Ɗ}5{F^L(0Ԯ6j5֘[HNO_atl0: 1~j,l@m '|2dxOIK>cl,7bdq0Sygo,i:~YĦ?trcb~mE+yǥCÚ+ay'N4ѳ{fa"x݄]`6, G-Yeϒɝm[5SllsaykG8U8əDƜIzfFoͳj`MS+_/["+^P9|'| ?b- ZB~1v.{`AzI7K!/dK##"rk<8_}լFwf+|0y=Ns): 'lHQCV>݅(ӳäx9Cy-W/:ľJ(l]н1v=_-'"XE wd8Wf -~zoW~1o5׋Ak^-<1J=4Z:'/wKW.RvK W|^T MҜ"sI>t:̿MhќcF7= %(-˻-=9+Ⱦ_ݮ ؙA2ϲ'V;e`M~Sfg/ el:bc&f\GKOY4-z|bئ 蜽~nx_cR&}~p/Y_+iMI Ń?, oyYdKNQު-G^-Hg5Wۯ[3 .e െK#i}Ue hֺ@/f|S ^{.-"\\eqCvӧOg_<_o/?g[Y+_~[oJ5Eefu׶]1r s) #?&P1jeD"e.}x/ZUf~svzGwMj9ߕ;r v1ܷr6AiPZ5y8?c/l_p{~%ʼn6/ei`yԳm/{*#;cjoBD Oq?tw4s()ri+ $p3D~"?z_ܰF3gXnv?׽_k֫ςy n]O$fa6 zx\|gb=חo2pۢ).]G~9μcmwh3Pc6 bܽ}MDS}8?NglיXKdʾG;sYr%d6wzܼG_*0G&aCqj1CRµ{wrkAڿq0e\E/6"Oj/>@t|;jd\F§K) !2϶jz>A!8X/`5YX9 %S-!U|y*UqW* ,~M|JuAe$Fu?qY: l#Hܽv<:ynNbfnWt™p}̽j &۹ەƉtt# م[?|蹧j!'DvG]KyK9OV/yk9K=85 oY IG?}Ra_˞E4\ y ͯtĎ[|?ʥMնse#뫆\/TֹN^>O>G+MøMyu^\k8OOݚrF!&g&]"m5{z:ĥEFhČl'Oyɇ~rAa,<kvMmBkƶ4G^|] k!)m;#ʧRLZ|eC%<*vj 79g>a]֚@|X)xAD`ЈXgvCx.i`>WMsP\c|rAGj$X/zdvIԘGӥyk(#?,@|?7tC/y 'Y)R0\dh4y|><^todC{gXl}ZfΎr?uqrO}/.ؕ}X/C+t$QG~o6VX.r oymrFƞY#y|1_|Bٺ%8u۠1rcx/oxزOn3xI˞FJX&ikFdY|]!D(=GNLd΃T3 |Jnt;SYœ9r]urqmU>kZxkGnA"yBo~1k7}zy⼸03'1:O3fa37u Gj[_w-=M!;eLbDCw|bӕxHv/5o3Ou?za5nw \5jywgI>C|#J|bG.{寙Vٍ}h#Y'q)-" D,ۆ)ؗ6n;s¼DI6M¸zldޕo SAG6lə@w6iztmKwBݓvGO ?IeYsydhMOX\|> a#xib4't >pԭT} ]K]I~Q烤$( eVy\u ҇ġ4 J6h3K8>?إ=w OggS[[ qm!WZ5 | PI hoO~SN*< X4Xv"UB6G\^d+wXSgָp3wJ; ǀhr|9nI퓯N~ 8Vo4ջTw~s}INý?<05(@1cxPX&PGϻ]JBo|[KB7|!zѹ]0>|XONQ=s({U}<׬E1ieٻssRYF7Iv;EFn_h5EX ů*˙'}ߟTؾ]&&2];Y>)m]_7_>Gu9 qC{xu*7 !yT/SRXǺSuGiJnї.м9q{<-I:jџk -KZS>dq[Ju~o` ;h\wDm]6ݣ߹)a^OTlB?!n=O?Ҷ)POf_KD)a HEsG`7ttD{Z?Lsif\ϹktjζV~%;_qp7pZݗ൯4}cٗxٷqCUk,f(xRz |26C2H^ӽ}+ >|l)|߅9U^P͆'jc9VNxrH4ݥvC>xwџGvgmp;tjJ,LuL`1?grO˽_sߖc\8ayl|V;ӱlk{q[?Uِ5*Yꨩۧ$dm-4 Ni0'%wwNk5|~dP܋؍b-BfB6:^ ܽ|~_~ˣaR^)M9/l&ρBUO}w?Z?zOsot&_zz'z9tW|<=iI6I[Vo6k~|'O&g.;&Ν_[ᙳ|*9$.җ%1CRZ=.hӚy'\OQ3ZPf:c%q82IDǂ 8Yj/|] 6No֋\j]>y.[M>IYo+zVk5/|V5P^? gҟ}>7w_Z{;/kQwxUO=U9E@uq}Xv"S~s vrz_ƺ?};wt~Z6Hɍ=/%R_q)oonRy~g{b[$NDl3(yY6UPMMLq4~8w˕qR&Yb'3cG`cct7mORѩ>e]l~lbyo]6N"|6#? ExxSvj*<*Fޙ%j{j.cQA9")W4u>ZZFzϰ~n kb?ۋĘ9V.u3>x7ؽ"oNkwMqŹx׳.:YC㐟>{݃rXܹq'sg6s>6m}r&?wqVaB8y?'"my&ͤKZAs^bygYf'gӛѕ?Okhu8gMԅ/-] f!ZeYhYR9Ih=9E-ڈךm|&C^\s XMyM^kOOK_&=i 5nRpE1 ojĝnQMG o@6~Rc[m:!n_|lmòn6 $"YcM@QoX$Ń}~t-Ssz,ݳL" ōgf/XBͪA\>Y*_m,:M~RAIJ+51;y3gT^8}bݝs% ՚>{7f{Y\`_Dll~KTWI@ẙ)+fK!ЪE/%`Y[gop͸'Dƽ^:{C'Ik{؀e4jrX̎XXC^K5ϦWM{{}MFqg9DfmQsyGAf֝>qo{F->Mgm/AL_m2k~n;Νʶ& dS\1ǰ3=:f}Ny5na Rke#Ҁmy&hHӏ?,:꾪a@D]$sO{/:Qz>}i{cvQ7< \!2hI& VX,eO3-vs{ .~ WpK-p"meG=FGg7yk{҉JBZ ږ֏jm䢉)}U/v/8x&7!su{5x788{*m,?ӽkz }PEV:x:OG?U_B|35Ǟ/;c9ɝu%{q?ӽGkVz"rK36y q+Ve煟VHs=#ɁeQ<k^~,99}6";clۿ)??Z6&O:gfQ SL?_$USVnYXɶ%{!̿-J-6{-,#s'OysR;ʼnILz%IMmC!~gj濲| ؟բ,Z,$OggS[  -i a~SC!H\=s[G/y|9I$H}1f}[rFNmohjuי-<z+⾿^"ʝSϑyQ(szE|MLOv"ϖ=9sLr+=đ綌'epf.5~l8/3q>_01e,K.|n>γ~OXhWVr&<]O}ce0k~׳uއX,멯Mɇ.6; 10{zRֻ.Ku_ݰُjOwx??t՟2y4g!8~:>yM^\#HL&#`Q2}FghD|;tCm4y,Z}4IwmoWP,q7܇<~j?2| +3#˾͇zvFl`* j~CH m|jS/|dF̀ύj<}GotM:M].Ԟ:c[ ^/pȹ!MtJG%ëoluY3t@[תGt/W|̯?bٰϟT r`Se] >q|a""Bk)Y]HUٛ;~ƃenj "E\>.m3 a 6}4 Bz8>+`د^{`n[ G;|=_]Gk <7gr=%D"9RTycL[{Z@zaH¿H۳ɯne9'H,3;gH{yS&=V ^gVe 6Oϩ WGFkGΓ+zOI'tkʼnK̆ ٣tssCc>P{< bgq  ;6`@{q~6F%zpU2>n Ut$aQݹQGo}w~e({|秲T=`|眝z2F]g?yƬɶ咙5;\jkzt],_Bw} n*K><']Q/bjn+Fy}s]npaţcaLR^l;\{rOh)ȣߡat~H Zlj(%MI?'{t)yilqszs\AxmDs6Y@>98a"# q:x}dNGl VlGRgDDc?=/E&T^ʄ`ϒ W @^S^>8vcZ埌4.5F(&o\O]N&DpoFO5Wvj|?+X)!ua>LjLM=ƀZj©k wMwN_^J}*Z֪9-]q>}k!kyٷKeT7۹^Y4\:g;_[[z%pY#sd&:oLnۮ_Sj`3_Q2^+-.ɤ=vs{UޏܺiW3E}i9s5i#BҷH*3/Յ 2sء&3'09$yAfv_? N ?^AFg?M>EfW1Ӭǩ >)|9|=^ ުI}ͅGA%sbsS!; %//^Oyjɱ86M-/8  F|Z WBG5}\Z5kTLs{l@Kbf$*O Zuxy3 *ŹgU_i!qzjuk9Xɑ?t7<3@"aKX<+*Gu\}>l 5YԖ)0"_,W\N&a:gI;FZߒKs {.y\oO<+6igsEϹX7Stv]u\u=@sޑnrh(>,_^Կ_"޽L>wnb"bWxZ2}mߗ ǦV`;Cr?Wp~xCZ ͟%3JKR} ~sc=S}d)9V@*yeSfh]PmdRfl? Sl>uj$[k*A^juz֨ Zgx?W+_i-6lis;G**JR&3`lzw͵[78[θr#/[[K_dC-ThxLd𸎗eW 5QlVϿ_Ș0֫,]2^U(GTcLtzf~S݃uW~4{l]D~;#"6MڬH\&'OKƽ`2K+>8Ϟ 4yySvgslG5N83~M^ϧ7!Yer~R6i:I}dx hvc,mVV9eL5k3Mʏ4u޴S%gZi\o>D FHk啫OggS![!J9 J5oZ|A{U샭bwc?o;~u^Zɣ!?m#d0ڛ_Ғ?W+%E.ksDuUb7]/\.pKe\^h]Y}Lwr]?=@ MmFl@.:~[_Cͦ쬆Lw7_Ds9pǦ9*ez^ f6}{ÿ$1ؑ*"7f֚ TĿC`$OX[Zs2Ov&Ldi}MN6^[KmZ--\;"Egw̶=Ƽ/`p^uI\X"оlKpp/9,.SǠh G:5#dgvcﱵݒb}?Y& Kqq=e1.E\Nםy 6 oS'3=REbYs_Բwk/jÊ^E#k*|hʓ^cO__K\R;rww(O|4}[qͅNues߲o,fʹѫ.EZs6'&:}3v9>G'OX"ХD%B( , AO(ї?.aܒ m^Xm ؿ^E|F'1!HqϒXta{d5rk  D sHFE׿fz8 `OZOOO`niC֗r\@~Mz+^Wk;r>u4•s޹7qnzs=V9Q_|9BƧgݩo3k)ՙ{c/6Mi3ǖsg7Ш՛: c0R`^Gqჹˢs{P$Kt"*?k-Zfʺ.0&/O3,3ļ]/wȱ3Gge*y`}^b;_\mS'"̓}:5F_O4꺎^V6X|+?e[g|ly|&8&bvI>3sFaF]-cN} ˷c[Oq!xY<їΠîm[Iߒmt&NǎdǯɌ+Fw)%~:LgQDj˞ԙtޮu?2=@9;Pjh=eP%-zG m-~@^i4d#} |+ݟnϗsQ@10<r$1fzFAN6.|Qk.A'"}mk 4@g\Xl<vD,/Oέ󗹬:ktXSQKdbk'nO}{X~dCQk{xCwd y'~Q .53wܟeǬH}Zck,jA*|GJܻYf Efя\m߻!rD}'^/{71ɡ|-n4o*#Ao2v ?ܟ9uvҚ]96{.^DX>Fnv|z{&u4[ƅXWg:^\g|Rvl?P8Qm%vaUO+ ߛJ{_u)n*jz{+/Ή!!ā&Db 4,_@@(8f |@l4 @]ݳf3h/X D$ґNUύ]o028śYDB7sL wD Ev֬_buKf[MVR=K:Vܹ7ЀU}lYйB^mCSf^s,R9O+haNnKj6x @ڬ4@{Ūhv;5fsdO\y7|mfPn[,a3b߯J3]O'WBc_Kfq$zڤCL~'g*shc9O8$UOY7d G#啀) JI֭ܥxTl.w Sz.NJt\Go?vzoV6G SMj1'fl8Ntv+KiAK"{suAu;&*?eA84-#<@~/:E_sPz=uⲘ\'kAܶ /Rw^DbR4-뺿r.[WH&7z̞9hh^~@rH^%7DKb/_I}>X֨NگJf'%y3r63>]ܳ|4?\͝u~^Z5d@)#z8EfvG oӝ>briQ:m5 S/d#" w3f{֪i)ˏs?ܼf_vT-ձ -;vyO-?nT[ b9q[z]4Rs{%$v1S^3/aG-QӧT l΃fih,SĨN>?ϼh=۽߀iꩻ'$,N$(<+qRuDP߭I<466k_9?fxԦo]7.?fؒyrwR?6Cv>֦k9lbm_=L#7d.Rn?6:j\K(C@gcCpy{Z[\>f6}J{UGd|(_Sӓ1% )|C$~g{jGCM`?ٲvE˓?=j~Ûs?wl#K^f?'.?!^M)5ooٌf2!#MJ96K/ԇĮGʟ4=VU[9*)oXg}wn ~flmGJ7S2`>˗/݆6}Nx˻2{F?lJV 'ʚB|]u: _~Mo?K=5gp=2(N7tW:Gzyc$E.P4|^͏5|І lõE|0-NϬcqt\26&.mb@k pO߾-)W4gapuXRz,6r q> MOd<!<ΜcӤT.bquϬ r|QS*w'?'+$2>kJMYT ߵ;W왃&Sߺ>=4s4f.o깻\d&>+|݇K? zzZ! cQP3c Nxio-9 !5>A_#"{6q X|k|+-/W;Sz]VYd^3 PG~RI;WGO<pٙaO.eÿ76T1l)?y2Jүꃬp@^8ٗ3ץo}.cQoˎa.Ԡȵ6ZdvΧrmme%OQGJ9ؿRDBÏNvQO^GK&7g|a[܉?qʉs1݃B/<<47gdĄcTb>h6p71w3369%Jx̯/Yvvw/06[6Twc팔C>-zm<jRA'&3$I9[|9-_z~/coiXP`T[v>4K%.eаu{3߱ڤ׎y]\,x:k\ +os6Us~CRkvD 7͗H% "Oy}0bJ$ntx{U]*}7ZX>{.zr g.R%-a~k˜^`j?^'4Iƻٱq? {OF霺Gąwykzl +eM_XX\syGQ.{߼e?N)N7A_<ʿCjB|/rӹ ʵh-t=m;{kC׺uCe5˔U3%(q߽݇q'>NJ579fzGsׇ޿|o_{#{2wm9~^؇jh: cyoXo NͩsqM_Ϝ!3Pqxb ya7ߝhEo6+5^riώ%&7iȓx7׵[bo.ǭc4aSbjU;3 ~ $Fk+8">nu{]B-3"B2mհP7'UwW8"b 9p7em_EV y%UnVV_u~3%ݶPю݌'ĺKM ]vwpzO~Y{> 6"t-mczM5_Rm8?^Xϻ^)()U3;A]MmarQFRs}2;wE_M0=gxc)N.`΅.{G7H3\`a3t|.9shV9zvN$ Z$ 4Edِad1ّ׌ޖ{3!wӘy'sS^RvkDq%_OH0=h/vVm8n?$ylᙍ]@zARq> QBEs9+ǜoAZ+=S]Jئ*/FWc/ be/cfy8~j51UudWx$'dgen#h >\^Bwl<9}M mу3B[>}~sr?_7<j.뮸,Fݚj/Z=jFbWxmfo_m܅<_ #Yh؜O~:D#yÔ9#;;/qs5}_;sԝ Pa=T?c܍S2rJds~w n{3:vO[t/|xUƊ %]J'߷Mbr*΄|mwl\S;aO&n,/>~'R{Wf`;׬NAYM&s1O, t}NqcZO2i'uQ3uLh:>g\m3WNw6 %ʢұ>b=}(?v|KxG pI{[! l瘋!=dp-ZpmcJ7m0xɏ&~{<ًMЇ`z=1X6DHk'Nؤv'&J%t+:7ӗytےY乾/dIŕaExǾ'!;Z.6CebT zxY-ϴ~Rc[g_ :zdiL5DIP\FE ߇ Vӳg~:X_04%4J1u{Ȅ7*D圥#˜;ey%?A-ϸ7.<udK٪*y Of~{8*qIesM(C`;Ap * m5d-Lxك4z7;ﱺXq{2W';ؽ9k TTDK"&˶ zX쳷_-/ O3Rhɦ-iև}V_ C],n+F0mFEF 'Cϖ裄/4_6<94tU_hL# ~Yu^RoY+Id,m`Pk*j,r߳I&-!̞e)mSm{:rϧ#vϫ?\Q\"Js/[Xm[[^8s.'1 N0g^-qUPT U/oli~f/-wN 0\:FMo'y8{#L`tP?,6mvJ΁BW޳>+ZƶS`~M|n}l ߧfnxjw3^+3-=h𺧱zZLU.%R|na0sbɭ4?;O<yok[&"6{^Cl͓vK.:ϊ5{MnX&.Ǭi;{G5.5x.![#>AwĝY%5XZc8toz.O}Kq(\dYNnXܦҟ5..:s2Z#eCsgv|ϻCߺLJGAD&؆Z)~&"du.澿Ox_G}\Ǻ{\~蹻'- t_'r?˟Y4ƞig̝XzIrgrVZW-Zoay:ojHB)[{6jϋH,JͤoAཾa+|i>}Ʋ} Ԁi5Uwu(Hh<|pCoh le([׆ao5 mza{_8^f1P8rZ^ذnH9#Mz3\sCƯ4rn;ϗiٖ-yyН<(5l{<3!a{BktPrkş<7<|}EYnzε鼖{^Ƞ5Ty!u(rNn3М<}%o}hOLe+awǹo$rNoncWiSU.Eqq~,o׶rK.I(@Nݦi(-䛏A=P9}h^*ٶj}*NɚHA0Lhj]$/[^n`/R*APO\oRn|Z&YD͗T>D06k@L0wȶ 1l{x.֓M4Sb{X4 ;׮w%Q"I~{Z2Ԡ kIJ{Z/-AZ{vē;meb֢>F:yR b}'0hȢ`5c}7E`=G6yvddӤ~6߻CaK/~z~%DE+C(\}?{k}ݿȎKf7:t N4O頟]sta<~WcI#˟LaFb;NtY7l_g9[IǕ(XL7-3ߎZnN'ͻQg/L+)rKLG)#6~ C=nZ6OFFė2dԃ;M3}?}V`ףZjf|)AL$Gl٬RA .^[j# SX#kX~y8g?O6)16ONGkH),gշݲP`=cӡ^hYno8]OfıMb>h쐚#>C_&T%•dRok!gFux=ג[6MKe|ǹ<=,_ώl=>}ocz[Y_}g:GAJ|?P73>|:4pv]}7[J{ ~ɶy9jYs}žg  c; $ohq'ys> g R(*QZ+l{ng -Y法w xo_4DhZgimm:G) şnz+^X^ 5.V"8Ԩ &"qZ;펇p{ʖHW1W9.,AD7Ypu1ypVXny{(W# -Un(k+lV ~k~mG{&=xzJ̻C7]Xn~$6 xjfьjܞu~ģyy"W}ōX5Upaŵ(X=@Xnw5YY%#m}{wCP־!PR:kF~fe}ZldH(x*]N͎9oZggUAue"/ѩ5 +5@h"k4١ji|&pG|I iCI y'*Kwʬ瀓z9ot7yIOܑGg{%f9Ĺbm ~F>hb)u!*3>Ǎ+67MfjYxCg)).n>rAުLQKG/Ѐ{#]q^meOP7ծk?g^!<ٯf+Knӝu9k*'D}>Ρy{~O3yTg %ysFF+k2ɁBsr+$dn|?;5Fno'^]nͥnz=Hq}Fsc_8Oш 95%IWbf&Gwq [= ̖̟\h'v'&T5XqCS4i/{fܘc85:~wP yu& m[66采=mLG}~vsXg",LJ뾴%|3X^ܬ=1=}l}.٬{DI{\X Q\M1{ 3ω7!ُUr`2?*9!8i ymx>A,RƫZwj z6gKO6zYIA>j}f|n-H^K7l읲br["P|T~K&:P~R3d[Ðע$ժ{'P\lzüjfAi]%^'GB)q2r#^$i46M'ӦiSȾPdt!v0`y|:U%+j!@8{YPlˊ*7)˽l#]u{0k_~- 2uLf#߈hu?#. zRm 58 sێ GRwA=n O'8=|2.no|tÃq֫ĵ/74ڽ^?tm>7?[@Q'KR^W9~h-Ե6e:t!-E׳;#~nyjcvטƇyYwk4eʼnSLG7~]ηt,G릾?yϷF^0Cϛ]_=voڭ}t_`9ܣ/y_3`uC+رἎ.J{k< nٵO =R H>twNe@/l[O0cCʃ\?O1S67f㳵RyZgVb&D@`5Hl% +-(q pK- ~4n3ϑ;ܻt{sYiSN\L&@@2Ǹ%9t^V*vedޚ5ޭSΏ_twX9A6BA A0Vo=L\ϭYOο&:^̤XZ={nz b&sQj 2rpOܣ{O|&ZD5M\jxdRԼ!$'A LzMې硭R_:_dϞ e9G6w))Xo^!4=!&~]h=N {gmUȼ8&S ^LzK!OH2Iw<8~63Mz&"Vs(\.5OqEF@?j7;=$O߃"֗gD=f<_/9P̆,uG[eXl|6TNR׀5TC}PbHjeV.bTb'(^2)u?u/sV(U7xuޛZ,YZ ɳ{m,{+gd?o>fg~_rC6mt|wp﹏Kyxg/1Ͼbֳ#8?aYvB<̻~i8ӑAv:3|hju'qh-!d)C!g[Ph!ؚi_~b麙+z{_' 6X(| -M([mv5 !gH~<>xa }~@&Odw<=q?Jm)Ap=_p@t#n'}/Jqz֐X̞zrq$;(<ݩ+=b{m?;K{ګ-~Au&[lΚ툁烹z>pv^O,Mkߎڇ{6:[X~(M#Y5%!j\tv4J qY=Qo%:zb˙Oc9ജb No~=t@~9,\^ P7=D =%ؕ5p5TCZ߿(\,~w\e\D+9X^fݙAwYNX3l2b ymSmppX,U{yVXOkoowEʍY57V>ůs2J~.u#ӧhZtftt^jN<:FhRM&Muu0sb?,k 'ɥkg/S :kgWY_OWA֓q!cU%u5~)g>4.ug;k"9z?k{me{s=8zӯ` ;kuMXEs"[CE6&;̱ͲNEZnti5Ϲl "~3(µ%A%\bfvIrhhhcmczU%Px|J]$.cR~81k>o^ѯ5ʊ)u%ywgStkևᥲ}yY:(}Djg aG|W5Ͱ`YyepYXtdu%׈29oW|"o.ͧD,O =As=|2A^Xhi\Bd|KۋtU@K@dO{Ar2zǰ3{_5>,/pK32>4$% w_vd%ߦ>3@dP-oRܙ|x|{Mst\ew[s-;fcIX4l%}t7l&N4$e;[5I=?drB wHOggS[%.zڭ@u~Oާ"9{]5bq]5P>@OӼp\IHX"q_%tL7d,!80FOyfok2RFxj;u:~M\oW}iu-26OE#Dj\ccS>~)w3>wWG数[ ˼wOM^˗@39Qn.eoCUY|vc9_2wN??c{k?Ssm>0c{cc~qpR7`ԫӁ{.)(.u ;\J(~]=@$?4 `N* 8V߹hj;L/g5?;kfRuPh+z:]N]5GOlsW3|-;TDalrۥɐpʃ͎.\"?>Ɵ[ޫpR暱m5]__J'v;i~5 y;sb8YgH"<}0ЙXbs!~e=>X;{Ҙ/ߎs! >8꜌O]sO%C +yk_"%wW{0&>.:>ԊW /Jkm_ZY4vb!og]WhʡZ.g^RJ!/?lu4 4f"1C6z{q}i+I{*jx6دO.k}{9Ø{4r5? SO]:D,@hھrZX0T\027qƏ7AqvDi/W\Z}&{q%s6?HUxLr)}ysy~'g>͡d׸lt^ ?^%˾nẃWbU'<=;e@ԡ9לsAD^ŲG3;nDiWך]|v,"?OF.]AzЃ_·mpׂ; ͹tbDx*{5ؤfwwϞr;h4< dTu5Kt*wos_!5er@hh{8fmY6IH-0 INP +r rqT M)Op9Qw,7$vSW9 k1 ^Jw3[=MݣT% pplc[\(fxp|>.1q1Pok1^6#nVdbPnD]~65L,.7)[7ކ YHhkK)t}{tn 9mv0ae\Z Vѳt\X_u|,93~4ɝ3|ۡܯuoZxZ1D{C^di-⫇NyIMdZXC?6I̙yߟR̟>p'`9SnZ]'?7InZ . ʋW֍'F,Gw UXs[m N1gM%~>whmHx}9=sCγ~B>8FŢs^Ntg0eJrgagm} wqdM- \wFg=7&=ckޫ׺QKxQa5W6Γ|2^R V,ǁ}@xO*Z72}m>j]3@cu:$$Ymε9sؗ>Aߨu{!"ffL[o|s;x?qǼAՍ^P%!ۈLfy߃vpNOf9cUCh;&PZB*Em_o*oEwhOB8%TY൒R⟚NzlFѫ:/b:FMzd g*mr^4s[ yYs2d{1)׿1+*։Wh;E}yewuwW xJe ߶,j}%`{?u@4vng,qsdI} <45Q;8v0 (a8[2œyRZY ۱61/g{j[:dԨSE3rCzC`bs 0-ohme5Q2 9[dx)C=˪6G{w0wM<ʬdEg2mӵ_78.,-khR>eRʜaY-2sݙgb_K継3\>0vNkǪkolgygwvsL^ԆǓC{4o:’Nnٳrcsm-L6@ L~fdc(VlyyTP%vmG|te6}yAF13KK.C㗗kw);[($M3:2ߛ^-=gSC9_-j~+2>OggS[&s?  |HD8kK"k5b>"?@4NSWTdl5ōhY=-I\vWVHZBW7uiwl'ç=Zwh߇gyXj~>m?ؾe_9PM77I9GEE]n:wqi\vYzyD,==O_?;X^Δn༭I? $qru YĥwޔcR^"IfsvYyXEMoo\eXB\LDMZ濷,yb2t UKwOhН8A H& urӨlxR ʩ U`v<ľE;۞\"sE}~Y7{]~g3wmD~wܾCA՛Nu.aeO/ˇ!zKnRJv}5˾EVoykm-{GF\'ZKZ~VqX~xn<8Po=HVStzPl/Э*G[*[щT a7m[2bi#yl3\@?EVr_Fd-YʿX*l9?&!g2 (IMNu>zxV@r#Zk E!WXAC6|?Glr'hRRc$,2)/-%ϸKm׬Φ]dhvufl딣Yߩidpl:m B`Crbd~fl+4{m?7d֗݇ݪMs0}q&?9gk71YND'>mFC Cziy[g D΢o-|ι?Q6Ų.3,QJ\߶ϲvJ q}ѣmolC`kدD-DL)R ۬Ѫb$plk#4g[kCv ~ʤωגtw8~!?}6~)~/i3-ᄐQ $nH#:N&dmgjSo0dB+fbcF}Ӽ{N} \w PmcUuWXJN2UIML5ΙJEN+c#e/*Mϵq9 y%/ss>c+w^s̅}yUF_fײ<瓅)ys~8m3azl,ʣ{~/@wl&9Ig,cy>u3E{$ϼ懈K?EJgnjE_jo9kõ `S6ګ[wB[Nuvm+ l !O5Px|zv.:kUz+2k(b;iQ/ӎ M2;ܜ{^F>8ԇWV^KN^kTܼ.R=}F~V]4{3t}sWUUĄITgvP+n<`.xŭxO|eWWeo<P[{m^Oyn-O z9 75`i$=fk}^ku(#1"^WYϿ+Ko+/@켴i9[~!\㦡_ul˽>̾W!}wȌ(%AA;윽W1Zji)}Mne&ןg>sV?3M*!Ҍy,=DD4`W|Kg_UFMɆШ!='O¶;x'3 k}yϿ4 Peo%;Wk=i=ߑdE,ƃe-4p^2߷{wt& I E6휙Y̒$q_+ 64K+췋8 o39<$=o~.k4gI%;u  ,}Ÿ8Yl_k $;G *r]\:v/tKB O^Z_ӟ`5# q7x_Kip|e~˒\PW麟Ϲ:cW r;QǾ%grwx ćR}K^eI6;yC>m'vt MrȆ{'ӧ9_vф aa>,:_'&٭9GOo璔Xuqڎ\6^m[!ȫٜzP7H m?|zw Y`no>9ᦸv6x $5?F#9E0B)a } ,бyKd=n?bccsx|oi3H Z_vG|rE9̲=KOhKK9t/Gq<$HO(9< QN^)oefMn>?-8ݗey&۱J̠ח\vg%?>ORoiyL酈_?oNo5-ZxfKCE;v'OXc'/0Y9n8[H笮ZK:yGE~oOeԛyoV| B´}ɈC " 6/b"n+[==<ۿ8&"Ic'lf}$df:%p4[}떡Wziܯ6.#f;6ڝ8;UaO%asO/}OrlX42ֽ_ JRoቚIOs'(ޥvut{zsk7ʓ߅VR0+ߤab9߻$ǘub:Nycl2rp/K, \xd[P-{Z?9YZ9|ՑY]|.ĠXXn|3{ĒhѴtaOGn='i޴[OfDPi%o lBmZm^*0<[#syv% Uze$<-ޮl)qDPkPQ)Ja\+!rFN%OggS['{6  M|WTfmX9'e jo3Lj\枦v* $߇<K&ymЇL%ەO?oKjBNn9M>ٻHaXE3}G `\4^>)cA{>tpדּUٚ%9od{{IZ5\ö|wl);CIfRf8htߥ޼6DW{o|]hkyOOOsX>5ZǙ/&sy 1.w͜ϟaNnL"1ea^xdt@q!bVo]"l%qLg*nj9z?&4F(SAӽ$7}4ilrJ 5ӶzIP@K -^lStV99}hfyujF1o `?o_1Tw^nHKsv;2 $c1}5ٷI_2#'P4nSMSSǎKu\,a+Q=TZN z¿Ri\ZǦTqn{7%kOzhjLꥅR,§kGuxėLD;(g}8<Yj T&؇MfOnĨ{5|ie0>/en7oSl\hl`<#fvaң0n=rZSknh#2bwϧfޟW)/,m}oa55]Iq'1uWBِ/MKeoGr׺ `5-*ہ{/φTj )4x/t毲սf\{~F͉FXSy>^!jNjSko/ap8gZ0?d?dr,ӢcN٧T{#Y{H: m{ >/MS d݌6ran͹cݙ~_b$C6ug~f۔ D}y[ӧ'dg3T%,ż!6ɧl|> aض&N ~y&s 1ͧˮ螱&ڧY͍U{9WJn3ԿvoϨZ!s'yv}@[קϑZ>Ta[~ܳ|.o6\>e;&?;. vG3^WuKGY9?FuuSBkL33(bQ:&Zk-|l?WPR9]ŧ4d1쵞`c~L!Nie],g|ɿ hD埩u?,Lnr&O揎Ēuw&ʜdu}3}ۥϜ szSھբtzz=fv;ĝ4?4Xgy2jpp5Kdш/q8KsƔ@%OZy9mOrW~pC%ި6@ ˭Ϝu{Zͪqc'7uwWuEDZUB\d^#-/}^WyOyڵbJՎ_suz'|8[2g>oo"vCR){M3͒H߸fQzޭ]%:3 &F9\%&OIF{Y{y3֑NUl3]αO|g\k]Dt1e>S2\v\#>{L_H;y2 2{bf>b}^>_ER I_Jޛ4- iK6ݹE6x8iQwC*= H)?ҌƧnN'9OL:U˯!L{TߞF3jbU#Л6`HMCYSY}|IRUSQ)H^jx(vlŚVqSS`s Ю7N%%&.~̠D|vXl_l~^.;'*W'7n$K_T;,7[ ^J. ִӧ&Q6@ߡ|>'/r:k>~,2Ѿ\?9D?O݁wgɿvMݚk1z/yzOK-;fj;:iO?=Y慴?=Ɗx@q}}\vwA<X[[#ƹQ0`:džiu:k՞#fs3Q0zi\bWU<^[_E:׷_4gElM24tcn 76?]뺒K1a<~w+y!S\bK/ڼewS\o_:`^`1 >:/ 韧/;~G7KRZCO9hή }j]7IKyit'AN&D~2+(4Jl(K486c/Uz%ʖ ԕhF%Ұk?_'QmWO9v/6n,kk?ܙ2z_) J~قF,o'5O~J$>!l/PiW g\~???ףxSyU%zXP(B|9;9oo*⵨=8^Fg!g;zN|PE2 Qk0Qo4> _'p13_M_?ԣ|w:u^>":]ǚ!POMy2A^N}.ϒ1qN M6cy9c`y>߭Gf0=qɗs]y1u^*Z HI<2S{1ZkT՚~mo5K]u6+|hg]D {rS1{s>233ĺΘg&ֶX27Rw ׼I?;EϑV(Doδw/n6W;V F@1nvfoZg:6Pw5aڐAS|W5Y.(YeOggS[(9<  ꭩkB hԐB07[0+6E}14G ` \y]MX9}L1!nR O7qͰvAc]3tż-.~h+PuGM^0#|zۺ|,lz=\tz#2kjr87Ϭq6.=5Wރﳼ˟ubt7{yO][Odz#פP~O6_ k}iqޟ_)GDK1ר7ɉeQT,@u''^a}~wy:v4aLz"!ie͕"GuNje1yg V8MOH+ugSgeJd.I}yr/fq7_L9? EK?;{4Ws7i$LeEzNnCvgϧ:2gzPDc|l['MZ??QAn} q;8n~wNxqmO)Im[i:C# 崅$#?g\>e;,ұw<ΖzMʆGZ EފfkL| G.t>^;㕃&Wo~nir]SbbID`WdE4>O<Gl_>+5u17񎦩QTYowzɖ//sͳyqʈ\̝]F^r 2~YTҍAf^2?rΞJ_ܥVbh=E1g|zJz:rů-siVI#ηZt$`ϕ?{7g}gGkzz'Ŷ;<z=gYϋ-Ǭ ƞ>XOx-D}oJ'?I xտKilj^/scn{=oKO"cΩO k5Vce2+lFc_^<8?{XЈ _יoQ+]`>_mL'O|CȳP^juy𽾂$S={< o8|@MYbel"e5c'ͼ;9ɯ!k^ai*rKѳG^FxF]][ w' r)گ[gi/^ :kE -It~@y8|l\lqgHךW_^vQv^l"ا}lЮsòٲ{ŵ3s93EGK\D_$?^Y#Z=̓r_7&kjyrKq;ma9&U.ĭ6bNJ9Ź.ӓA F?ꞷ^|f_338ʯ8+ hn$7;!V݀-缪6eIe"%3/ d߯è1!Fl"[ijMKI|ki'fCpdw<D>SaYkˬ9Y^l\4fsasQhQ^)tqz?HB3rMW^V-y+Y$EZƙ}eO؇y۬Yl5WLָY\{<ýa^'Bjt?V)o&^\]+^9?l.N>Y55QsuSg c@L}_ˑ?CFᩍξ5NZ~xTQ+L'G>LC+K`R-j `OM CN# |M'ZQiNgfq{I%:-Ͼ]e S]ܟ: 2m ~Sj!OdBiwvzqhk;z% ͘s=ѕb n|Q֟Qe|dO{Ͷ_FMg-͉D߁m/D΃q|.M|X>sE1ͧMt}f}Qhe,?z1~#]0}/5rvg"4N^⵮ FX~l\\,h$ͪ]6˛Wry]ݹ7e`sND7\r刬tF},nyCrTR@?3sXȟ+n3peV)|  ľ>Yyf-;g'گh̡"&d4TdriDò&윜PӁ>^Sl|j<66[;&g~ENNu |֗B†HX<c厵|4ϸ~Fs]ӓ2F1W¾4s/*qP81YW?[&1a7\[9fss-c>c]±:pS7?e|u1oEG3:"\9XaZce͕ڗ.O׈{Oj@rpą Ξ[|`ݽo%V/}U2oHks>gg>*&q "X;?;ⱌy'nf+W3^N+>d[m/w'="ԯO3w9 ٙs dC0M1?L_ĞfP ̚d3 W `tj^aA*q?Z 4वVOYx'P%6Z/Onj̟ ,6 *pߚ@w`dROggS%[)M  Ni w\pN A2-糾=e_{ln7>dS/e:y$V*R6FY5mx^B8HFo2>Ylu~~D|ckk.wr1HcW=Uƞd}~KeQf˓<}u{cY\S{^p/oM4F,@Du\ھνWYuI3ԉJ~q,-a5v?lj.t1z_{laty[!}<o:]޽~/.רΙ;9_̎Zx;bTʪ$_<) 3Fa[mɐyidΗ/};R}C?Gg6qƤ⏱>}w僵R),b+S kk kAg"L\q*_8v՛\@aL^OՕN8b&`5"+g$ F >xj|G/x:Pyʹi|J٪淶y .Sη{y\^rs7ѱ^~'ξ2tTU$>sau>:ZrLX.:?7?QEzn}6cν-ɵ'WOjB?7 ѭd9bуtvAϙĵg -9w+q&T& t8m'?f̿Dp_ ycF=okR~Pc>aN'4X-rFV3_߱Z:u_Û_T(*ޚ1 ZgZCu4;~ WǺ3]_nPlP)S@'I#IۦqA^xD%kv%:{OOSnc/[_As#nY_ծGh:uhFӇ͛|/jmoj+rw_h)'%w<;u.yY~Z",wlvw]|.9,u vfwUe\X?~ ]) <,y1>t4GXvޯ{OO>gt| ;ZŭFِT*h4s'5Op re+?Σ`Msc *S޴S~*Tm,,n_R2ߕ=Zxœ@6͸"(( Q<:q|^˝k.=Ck#ª97;ž#:p pY&jY>fUgm,7SYۺ!Y_%/:U|o](G6^WeST~Gs`״|1R7曅Z},:cl0>s)J+.}TlΈ>>ztz{]glˁ&jD9?|r/Fƕ%~Md4މs[m-^ Z>X_)㺞_5q22ofj>Z s ~9;O;!cw_G7Hll}{&`ll&2BC߇E>o@zcXJ9v݆9#@k&Q9oԤ5Qy %,.'M{FsT $ۇmImQ[p^u|T*P/>Y:z{_\]_OxL7.:ILpiɃ {j5|BMӖM@hsx5Y51LT_k"Pp1UmnM2c &h1y7{s-EO{ $q.|_n듥ȼ,("dWϔsHdgE޿ F҅}rs.Xvezo|z-ƄDz]޹i;d|kx 7syϋ9'Qyw?gۥ)pe_痽cϽo>XƩ.>,9G_.L?=wK4+,?{p38=,D3M>{̋G$]Sԧd] sMfM4#fi?:7?~M3p E5m륅AY /'5o&4 xR4&R9|@[)Kt0O9civkAM_/g8diMIa}:*q?\(xEXK˓8cz&yS| md<|6Bgiv]nǟGY8?Kѝq3t&l"Z&g?Wߙ*lc +=3DXwZ>m%N̎>9%v%%"aGѨ0>M5?\\bJ귕FȽW0='OU8)%-)9NJB^[5Gel<R߼y?@I <$z-<^K)@=G lѣ13r?ǵg vi*.s Ee~ɼjFѭbd_ꚻuml>K-mp=gť:s=~9-J#Ⓙh īc/ʘ|ꕟ8 zG0Fxk?ϩ/؁/-扎3qve嵝 :'Y\c׉W!3g^[2sg{:Ⱦd:e/V_ Mr~{mG%=u^zd@6}..fT|b(zo:Oa}aΛ)i=6߫~G]qZw \N{N5}Y)p_C4qp(PqX/9YI%uM{+CBö͘a+S|T>˝zr{oir荞c,}uLzDtVϏ|d[+bbL^&jWp$"vh}[_F$R\ICVjX9 [Mtmk%wc.bӶ䑢}wtmqkݓ]>k:uIMg?NӋzq#y6˜$ '#S*,?YTE=Bt3ODs[&FXyGu/?v{[g^7FɜƼw`X7d-Gk-.y9,+LZvnL Sܾ1ާGr\Pď'yRC (!b_8R@=v&8r/Vr2`+]y>-G7xZc1X "゚,n-rJ@noI\ޟV^ڳUKZ~{ H`Gk?HbդdhX( ^wu:IH;%q?&i$8[)ٮvSo-VFק3̕&r?g+uXw7Ϸ Cc2~Ie91M-$>^;Y$\ec4%:ޟt~7o}޷Sp' ۇz!}2/4sw)2Gd}gz k|H 3t_/>̿K_cWZWZ~|&,V ^DԠ*O=W>D??`:\&s4 # ss`BUݜhf*[9MF3h ސʜ$?P')02Q|r,9jN~Dų7{ޖ P5^?Zӣ2QݯX<~xR <%|y{ϕ(9Z^6qQX޺|ZhO ڑciy';z{Ľw﷓m챗>PQC5I$L5.-G]~RٰWӤ* j}|ښVvy>|^01ԟ87J61lj2Ghto^7k07߁&'˸}zP^ 4|K՛j\~G2Xk 뼡?F<ϝ9~̤9uS3׽GdNwODwtt_{]gk.TSdg7 \Tm}O2q4)xNOT/4{YZ)xgX :Rk5)/ul=OK?qyhT2==~DCҝ-fHD[>nTpola,^b^r%Ky<s~&ss a#srUzhfa;拗^$Ɲ|lTW)\^:>j.agAŹgIK \R~wgI/ydd~]__rR.IևH֓4>1 oM{יA=Cv(̓|. mL/iZO./~-ipzF}D+xc0iҀ|嬟~$C@}^-8f O4|u'LYC SU붴 LH?譸Ǹ]%)ꮩgWqI$3{l MͼU'bHQk=5~m|zdy[:^|6[gC-봨qqY%\ k^:ye|t1#6aož_"cqdǟZWO>̉P?ܟ/y΄-+M_>DiEӯbQ.ImN=r }n~J#3K,/2/~ w2xw}lA&9T]sIj/UkmBV[/@ ol<7VrvP)7;u7iƱ]ʙ% e^y;O^m ?U{C_f=uGw&>ў.SpBj]]L>SyKnl -#ymYU֭u\o;T\r9}>Ka@֥/Pd^oHױ1|zhD.rt_Sk]E/re="RzaЄyd[첈5qmu'>,{ T=`M~g&$Ý#ȥ~2K=sao]^wes˱}S؅CG~r)j1qxح헇+^~ڮj%KDc:uTCzY{Fl2[$ (,LZ_UO5Q4&_'.& nj|MsYMazI]QT7?֓Xu n%6@}Kp=mzX]x9$XXs60yZpߟUO+isEjg1[_6lwO1c0lQ?#cj*:O$bR)^]6t{iͲz鶧|W?y؟0_ܿ:(?Mr{irAUu}%{^I{ogkRZ s@E`Z]O +^.j?\'KrN{NNSv~_ѥ~=s4p(wůo/sÆ}&w@Wv5'՜HLtDcJd(vJ9,,Wy:9 -M"z)r}n;HK4A4J5~ PK3k3)eo㵲񘃍 ,d%mqn4-&5Mjp9r{L@w0G@OggSi[+PL >GnOA`d*g߭ݵݬMn>g)ΫsbZd0g+:Y=i咖E=uf?-qJgGC6>z~keKeW.!<_eze}n~ƪ}~eIj]Qղn|]8'/!Yew?{ā${]ҚC3Ss>?zҲ71"^gxν,k}~E|r~}_OifI1y9zO1w"dxOΤ c d#.yӓYMy}ƿw-:IR?_'0%\hB|Lm"6:>P>yP׷e6Ӑ4gdRY;'P٩JYz1mye]yU^!1<,,ۭjhs|a7sh*ֆ| e5 1v.vcs&C7T4$ت([Y򕗟RnEM1o}?۩"Io;Qîm̳!IFц^? .|9{= syOu8|݃Hpp0q]~d6U:?0Pҳmy.qL}~Z,*C>WZcp\Puk}\/ł W8,]򽸥R<ұҭ&Cdg’חZ}V9[sWnzɣEێOdd)ۂz]쏮` |.̅x; b_BN;6`&gqA3:Mꖔ%\O.si,U% Uy2W/ox~Z|2B tfѣ+}3M9Qf^\CW5O~_e<z'[7E}+^q-jäeyɰKKlK;ߦ1qݘP0q΃Q؎?pėa~_dG/^kΏ͓5&V\]d693 _vpW.:##?\z;Aͅ+A2~"0R/TnmVGJ0O4Dc@CúfS$FiUd]~HбQlw>ѽ.?Zgk9x}EUwM<+M6$dҌ7LUW"jN|;32/!h4Psg7,jzq 3tDd_M7?Y39L\6sXڅ2]$N? ^1௕|}$uatNhGv9(pIg5ǧaG=S|5|vo ^~~xN6H`+`no,W23H8 OeOޙ y{ :-2*KTt&!w"EbAdzi}nO}ն#kl~;XN2)C꺙u/]|eٯo|9}r3O>YX}u:'輸<}wJ?5|cy)g:/uˎ3\룚YG%e&mJ.;GE²̇eY~+n2[ϛ)b夿jCfC*t4$AJv38S-ϛU g7̓oAB}+~Oyz<`ImM)F)[8-=zqrwp-knL~ uj[GMe"js;\̆<5 aD&ѭwP_b4v^ $O-ۓ!RX y<{vty÷?ok$M7rehir@vCFě/W{Է+ޝ,vvv׹k$z p>Z9?? [{_&ojU!*[x+z_>x/wC"-~75#c1bsk vuOyu$f&/ozf]C 仃6|Jsy{y^n8>k w ˓/'I`|Ǟ߃TƾwCXL(;m OLk£1Θ5[{2|e^М;0wJ e>Hֶ<VǤ}xgwmkb;4}QP:%=-fY޹QD;ӌ5+/ z/}F'g<ڄuvJt8܋ݵǎBόDs^*?oѶϳ4>F}0)>Ī͐43SQUy<ߔGk-ay#"{%N[|>S@Om0=~$SZfI^ L;uF|}BƊq~lޯc_3zj ~u=3 \_/^֭?IϹl7-N9_/?H}h1m_m7SJs밺jx1g&'rΜS䷲Zv12) ntm[.ZJnNٷZ{6x87ϝz57!])XmwVv1GfXOggS[,x ^t>tj:V/K{olqO+:`n {Jel^_!-%791[i)y˖n W>X|Yj\j6;EO;}"si A\K[J|G*ħm}@Ɵi')&&'7%g;SkOc~'#V~0G8Fpڒ1w6\e}$Mt}Mz41ӇF 'ER[*{=Szbi¤L}(NZ,/5o?m)AF1u=Q}ǹsϿܣVųρgX\II,,/XÉ1AOlO?>~ /u[緞g9)+ MkuVDQh:mxҺ?f@IfP]7C|sg3unK]P/|yߑ,Y>3](:\Ri,.k`?I3gtތmC^X\|['9r|GFmcǠɅr/{vY*m;6@\3Ī{96FfϻwZayձg/TO;?0qf?~>3?:V6-M˓O ɺ7hr= Dwo`ғ6Y/uҜYۼ&YPHziF!m6aB 2oPkpn:W 9 f$6ә)-n[}ىn5}G{5*zlll7tej{1IHOʆ7]~*#i]hۅI۫FLtoqc,CyiYl'MW? Ns< }M}K\vSmd?(,KbWIuAe>}3uǝK/;~g<*os{~+<9Ld#՝hxttG8~.>;JGD,CSu^A9&WmriT"kÖ4Q]÷g y9S6k7h,"y7#"E8)wӉQI;ez.{kԩӾvZʢ>_M2Q>P0y76v:lblP}D !!SoƦb4+WڿŢRd!oA@[R4)IRYk g~,~JΏ_/{.E,w]78V2|z>yur!?her{y躱øMv6<9yOYBx{gt͗qL],>5Կ1!.yn(Y!'V.0}Ա;M& 3t><ώl Dv/?0?L<V+1S٧_>d.!jZ\K{mkO $7컯\ 3z^>cL#cf_210b;7Mc\'H-W'vY y]G[ڍ'aLD?u]xsDRRFϾ[elUƞ~?mǜ% ı~aQ c ldSYH~Iz$ ϼw-yߨ Տ^}"WYR̜*/Fɢ1gHYYxz}A}9dc濮{hcwϩuuLFC7$pm8f)ZbHY :-1!F!!j akt_a` =j{4*cb‘*jIgӧcKV^`Ju)崴1_ϫwlqΑ@tc?;5Kf?3/^eHbYLwJҧ/9Z&HaÝ=%$[~ ]͓ŻpRǰ{on<Ow]ҋgs_~Ct0ߨq&3'S{"[~Zd/˒㾪{dY1-%W/Ѱ8IZ2fu6["=긌_~,c6j1mlDlLKJ%~ٶoc3tL9}#>k-V&Z*|<?8216N[a_o6L- ۣLOg*|9S Xs\5VJ] %QMRخydݒ (>ފ-|X* ZiƼz$Mk?׊I߯U6&=uu*zE(yL_+Wk&ߔAxߧPqx# oJD1e_n+Oeqߊ7.*ĽyTzz["6.wzE^4e0RerOɜ<4(u̞rL{Mv^}̶\?^BV1O4p=5^_:SC[).gԺZ+ ͺԛ'Jcz@~Um^O{0l5At281FϡL4琟f}]9i~$6pRH:D,k^AWΓʴf'D&PVdv޼^)e8gZJNj48o|Vϟ9uˉv=;ty˞̷] qq>}Մt;oZݟw#s7bz'[x}c2=EoE$vA|s֭aO3fb-˻t>f{?L\'y=smݔvlSo[~W!z?Ŏ{sf^_<9c2RD@ǟ̻BLVD.&qP.0NP;-YA #5~BE=KM  !XCC4',kdkXs[n>Z Z~#0 g}3nX#WڱW#, = `@wO )K%4 Voʡ1綿StO"gIɗR.VqN܌tbP7#3F]m t]tۆL>s;ahmԤ!F^9ٻq^Qٗ,w;.e[泐uw/r~ H`>6"5ؾDm5Yu]WG={Ͻ~_?t_.˙ߙjgf53}hه!߅ ݋dyۏa4ֿ9F_{1km>_;GR4vi,ǿ%36[1%Y5Y惇?a2HF{yi+ԡV`6'KD5,}y| 4\Q lI+nm A_Տ _$Oh.G_xP7;WۋK8\aJ=8Lm;尾C3s&U#R˗/ٺ?qRJ YGYL̀1ЯcOmD`TWl'K5Wco8bZȉ6>:\O|yݟBѯ_gaU oas^~O^~O]Ê:g.> |vѭ/DK&\'q,z?2.o8Q\_l;1eHXr]OpvqR`߉3_w1|O{&|wm֙nE-?5DfdfN`jkI >*s63H %e^ >9˗[4!0{ve:gK[s6ɕx\dH?@s82F4UC*k)׻ zo>k0>aizzNeL$P 3ћGӾF(sQv.ͺ>1wKmvգrb#"˛>\O3gb/p@nN&}O2o>fr/K,y2Kj kn:kXh"Śʛ"kςu7=93n@ߕ6=DqZu5}Px}},UK;=?z,G-/}{Ԙ`O:H u;{r r-YE4/lZkɼ_d=1|{vԬRn 7ZvU˕kaG6.k+ǵiw5F6ןy:-rdGf"{(#i9^e#r^/C(g )3ny+ozlxHח%#jlCff{%"dP UUϋ1`+<]nTY6ZbM,doTJ>\UbQGR߯[1jF[>?C޽y;8ƐYQ?c+~j5A^q)[۴zs11(;|zU$%L1 Pt(Y65lL¬hr<&{@״f1oQqA/>q(:͘DĘ[-igqV#70XѕTfx8q]ʞ33,ѵGץ|9rwM}eAߓw@bOGSjȌWx͓)} :uߋ0\~'qYx(,Տ ZLXhYf;@(aOHYjv6;#&$Li5o8{: 6']$&^cqM{թZyX]2v9x[}{n+v.A=??-OF=LgfT)?0y,[ F.@и_IszΑ9 pm2PO(smA*AyI}kJ%Uʭ|r0}M$@x6>?%H_lW{f40uvꤪ-ȘGֳVse=-7+%鱿aRc 7ןc&88d*)QG=MQ5vuzDw`%}c2e |\+1<ޖ%5yzvԹȵ>g4I)wuGB.A\;t\=S/Df>;Oy߷ zNSw㧱nKFx OZR\w~ {/w/E<s5_|>xTX]o,8~_e&-) ӎ~\}z'v|}'6`z9,ᅙlMҴ^X]oaC%p?7W杳9uo}X7Ę~7ɱEl;Hx!|Jك J]~u |Txݠ=>hV3a?nmgyϕͩ۴઼9TU–yϣ?g]2=Mo4׍N˶Hd?# r=sqeZ9Su4`km(^۱8f+3+gs87>b\׳-O+YJA s\ڙZJm\,ׯ'v{nz:ۖH1?jb-rn.E\6}$p~XjȠ٧D8btKj+\d[zٍWv(uZݚu#v\̈́ mQ{ZI*[S'ډ%D|lԼ\,x }OУ"d?ޭ7gѽI>= UA,_$vPE0,\tW52C=-ʽfyi !nw}8ͨf>]Q 3n\ ?vQK^<څKg}x\v4y u7rYx_s]~r.6\W|Yg t9t 2/ =CsgS)>&Wigw$^{. y)(E\33d;}Iq4= XG YbeE啩NƷFPvU?lc:}.?W]OTv1HmF=UѬY xɬYjc'lƒ=a(PW )1(ؕn,iA F5q/=+Kj.O_[X+#,m@I Y+G/9FkCQQ.W$1Hx{l_(Crt¼0,pI--܇ ?0P+w_|}y38}ģ_6O $Knp/o{\ϛ0甉ukp}m3n<Ȧ{sw`-/L}xnm}0Oj{ ],7xJ le YA%RKp&4å܊me۱1ѓ{sӹ*!6y5I%"y7'$&/7 zq1ARǍz{rZ#ݙvwg@9DM=~"3聱@?ll'3Al3="w^nD skНw ]U$O?+ wF(1הRU +K.%o?c{Nmcgߒ(b^7).}~}rOy/' '~ML~K qJ$Im,yej"A*v1~]%@Bmk_ c;ň cfՓWsq,. F"2f]y_({G_mQ^'p|T_}6[^Rsk:mǗwhj!pہ.3['q{9$3Y*=#sG97zlͯu%73K_317'3Hjۍ}?qÖ/ |s;!S{FsQ U徲}aL\_@f$Kt\(fLOo{1%93v ߔ}8)$%g_o.@Ԝ +dM[v%DS/s8AC1pg SU4,1s}k 1 P𺦪{N'.1P`~SV\+l`quھ+ߏݹ_~I>$1- ~<lv۴'GcU2O;'SbZjK}۞Zz}u3:CbmD W湓v37 %ݳ:Ti8M?Lqs~>9d]Ҿ^ӍM[?Wn[}JOԳ2>ćN'΅AX>ݏVϕ=?H;ܟlv'l4:+q<.2<| 1y?A{ѹ]5m*ϼ L)~CjԵu *$[jB­VVr 3ֲr, k>6M`NU.t#O#M}؈fKks-~.CnWq?l-7[xh/?,뵖'2H _(>堝:Ț+swr8eu{36kKKx_Sj햧{GzB6h=Ν._>_;/ ;ks}-Y۟F~}\8~Q~6ޏ4fo3Kʲr.a1}.5+'ߗ1o+x"i[lz6e0IN癛;cm;{b)GDרiZojF[Ê_g#K_Os<'"}mLjf`8iuFjUX,B0P "S0xzlr[쑇+M- W#/\ qp)%^ÝW$ ~uLp_t ~xM>=豯ǟW9m5}^&=hfuOՓs3ۅNcٍ3 s⅛ q]gǿ)}`9bl+,kMdAצBQwkݯqG͇hD>&|wDv~t\lIĞw̵!=JȿMׅs/ bBiW!Rׄ8kN"zr ?zk|L*;Uj1t~ߧ[hsR%Cv'z>+<]39b9Sh|Ibsؘ6v<[t6K9c˝4_7#e(?.>mmW@2J}]1bz׬L[1t4}1sNdUorξ^N|?)^Kn%L(hI1a3'+\ýd=eY7З/ӓd2жor@6:ɘ/k?}ey27\ƫ}lš둛^oE˓ߎKkIV دKlke\۞ȎM^7CPg$(~o.x6i3R۾m}kUV'H:ǣݲW7ّAJ9 `E< 6.ӢNeV__c9gy *"yy9\d߯\>;\n͖Mzzy2ex#̱k>=SVYu_bAE"v^ȱT86|:#e;.t2/2wr=4/嵑u? }yHwl,b-E&iXkjD '맋/\vL?qAv?6Ӆ* Tɕ5Gr@͘U:xʋUZ-xϛK[.$[қG[=}KRNPt* $$\#k#+e{6Z- c׌>[OHMP~ڿfYE/S^k [V-n8Vh*J(c# ֟F]PCU: 4gpg:w@n'tVΙ=A5iw~mdVc,H0;TN7d~:o>z׻]lsw{bL \>Y,p_[odzSmyٹ\#etbdz/feߢ?g^^0B2rɆ(FvO+;OGAd?W0kժCT-Vèq~T():7?˕fϡ=G:T[@~=m<^FD?!V3l}üM75^{Q{*qbzLWl8;[9N:p5\ݢ}'x}~܊tg1L$'x5ȓֳ4-~#tPm6LH]ssܞK[=<{̓UsY2z `!z}=yEvl,cѭfp=iQ}L\jaH1_AC>HN})= ћ&26r]r.%YHHΘgO\Ӛ_<*%b_;ȽD:6-"Wo8L]&?};ϯwM MaZRU3@<{W)ه]#$0ny?Bƹۘ_ƞ_;M^\ v'\E2hf'=.~ #0*>Xֺl߸pWi>94䜹=,gV`5GR%~Hl3^m[#~`D#"iuz\}Mm5Pi/,IX$fnxxz\7ǚ>'/#l-?EseݗSkbۨ駅Ehcu3Ar̴R3K}rggw9d'?y~Rەɞvϱ(\q._zsr,2N{pΗbϝ=,}}F|Õ'L_Ǿ9cX;sutSƲ"oS%w!2l_Vdyϵ#KeM]9D?s^~w~}r.w Of"ˊ@n|$~.U&zl?,{6C7^I34}FauE!iiy! ~48C ]6("%liY̗S`FvCmC4?Ƭǝ6-[L63M&ٟnG Dthi9oh'|Lu<ɯ7RPRfq^kH742BbPn6G k-M3~E4zcI]@j>Up"6DH?qV;a`ڜ;W> g۹瓧 25e˧H<%T h.;c(!j|!ٌUuX%gzE_Mccz4Y\eڟǴͯ\ݠ!{*K{}.Ro;9.&5~o|(7_{~OdžFEޯz{i"2i|6A1gt=!.MKC'ڲ>-`׬3"#.m6\ߐo @UVU %Iz #fpt[ν'[l6~<Oy.试[#Zߦv bӹq[y9(oW!Ҩٚݭ;*@aUO2=v䙱gTMsEyn )BS)O!=i6DnyOfFݳ~&Y6;&R.8Nl۽<ȡo˯ 7T^7u2sn {f30tEU?u1&|pwaL܄z6i.},iJY:Kr-a_(}ߞGv쇞<߶Қq=N.qΧOtjGz=ݿ|.3p"OƗ3WV L QK%QRjZK1ܾXޱsO{!7B_}`eۈsm#.1+,?D 9ȓĶ-wG~<6O:"Q{"}b /[4 ktDnkz6[[cTN=6} 3Wн'3J5S;Q- ,xR $l"|F|Z{YFzxV/jj{ PvnsI8 oA{ ۚb#-© B ^U{/g݌YLJ ϝa=Brֿ Us|v֙}IޗE&K9~#a Er MΙSG9Si?'ʷ̙n+^Jv6z.6:rdrK0;Q],voCf~JRjqjHdX;MnNC3yѸ2WRMg|hz$R2'ۢi>9\1`7F|HIMf7eEMP@okiAD"͏[uR1%Qz& n{ݕwrw26\pϰ3v11ή<-4e; % .OggS@[04:5"݀*m)sr"vuz~_Oܲ>nzl+XCiPL>@O tUwRLWvً]|S$/wZXgBCٛV#OuƗ6 !r4n7M˛olne Y\)j2:ߨo{2 W&ޡg)[fSNP/:}G|/_ODwq/wn(9v81wjCgpzs)ފZiJ;N8ۙ൦ ul<x6gַ[L{H]~S[燘]r<>MgʁFzMrz̏-={x;M4SB#fp^wB7uc<¹FC29Xm?=~Uu53>Qp&NLZod3xx!'cxdg+f%m{XeCz ƫVcn/K(^M VoK-`~3$W>|Y.>`zv=Wnt1a CZ~'ӑsů;sFS8óiSkٓŽndwIO-<K3K_psSr>:dEk\ \އ+:,S`}#tj=q}|_"vfj%}e4R>e}q\緷moow!#_JZLFC=w8Wc_ߏe]h>eƸ֧(3ruO;secPsu,];t[zxE.E3/ dsټl U.=;;Q 8S#sCa3\?j2ZYahA\`!zY2*2)CA-w['IZoYˉ ?Z0dOD2.Q+ $-FV?0w/얰iK}bɢZ߸Se RquTE~[X\-3kԁ@OUI0={vgml+n`0Q5@u:9&b>ߢ.X(>ωxzKdr]_']+yvtTEoGcn9Q9NȰXOz-r.fN2䢗ݺ^ˉ}>wAF) z,iJYK)Eysz/&eGw̲o6'<)Ŧo_y1yEgںU[An(D_#Cd?l|Bx9's '#]S 1>ZEy˟K7k&o=WZ#pqx=}jAk7'ߺv Avx~ȵfW;ؙ*5 GEH 3S>ڍ ͮ5Q }`U1by}FPs@@{핯WsՖNb1p=WWGqk?v Mb]i[ΠjO~ k<-8X i"<ʭ^RaeLf3S?v:Yw|f_b)cC>6|Ft_ ?}^?t'vh/.]B\)m⋝}r+(f_2'~R9ɠfځG5^Mz3vBygu R32~KWՃr{fNiP͞c)=AYX-J*䙙)""ɶNG4 nǭ)*^'I?j-cM@s L o:~:am<wI4#BEv߭*y'ԿcM3o2nKx9%k), |ޥ5~wH ="rOuzkKüWn10U]UIQzݠՉ |VS7S*NYe#y:{Vbe67cdZo^4hEKF.?eD9>^qp49DRD56iW\ܛ]Xe: (fҺw sY5qeێ>H/˦辗u69|)H%/YA|8sяH,~sϾNq=o^n}Sjj{MOݫp4N;Ig[do2{x:АMIŷ\͝`>9~rY`WwhV?q˶OBJX|naiRfW)iu]̋j1A<'sVА=])fu.zm&^ާ2#d2L5ūǸs]ˇƚ ;5\;IRᰙH. v˫\Q)Ee̎->k=Hk3 01 gngh G(r8$X@X=-Sᾛl#і>gCc?Alwv~p,vl1FXº&cQ[Ŝ-ЌcVյeׅ*ղ؟}"L:^h\4qwֹ1]Z=];99d.>Krnx}qcT-R9.<ΦK& C[4&k(gna=Җ}&޺Gkr9o=-ǭcP[dooom&/}<:f:.86x(нT/E{ o*X/]lh\^'"!E,J6Y$́{r}>mFA4U,c?0wŮOdܚW۫TW6{*D ]e/4εǜp%>؜ J24v'j^T&M:1j}*0~C=1.=p7U=(xΫPR.fH]8Gn|hd[fR]n֗%ګ;m.H3'Pްof _f>VUpU;_a36?Z.~cCWs+i_9@߷4"\&/wϼhӈ%:/6ct8nҖI-$ּ˪2뵐^vL4b!N`Kb?f2nKbnq8٦ f6?&NwAٻ zɘDǧ\'LNn<`>q5׍흸D+==ifۈ"~ Kgj]J9<ex)E0ɗßxKkΙOznWeOggS@.[1j>e |bS+@}'~}Ϊpӻjp(]*E 9 iUuwoK!/9Dd燷Q[΋x~yꑼGδrS˴9g鮯K!SWdvcfeԏΙ{fDaWD}'i/фr!G 5NLʴd܄8npgzч.Y3'EQy #?eɗyޒ㤻3J)-ؑ3{̝;&QIJ| 7HXx#?ԋ}]z[6幚I'XR1諟^v-Įw\;~GUQjrF1vSŸWGцZKh6ZZnlgVICzEJȝdG0n;Vec>̛ƿ,,~@ƚ{9EAkMuR~/k>y9cG{}yɾsifF)lk=s?9\p'뉼9st95mh]}i˗gO6p˥y)@iTSkyF%&2 Orbm篦oe3~ج;d9mIjWͼomlfTR#;N"N =Y}5-h^W>K:F yuCkq 5Dz_g vg?ydܝTYRJ 4p>zo[,qW|c 4u2 C v/!5cF2^vjkz՝owu†NGnr\ۈ'Q02Ȩ\wWy+קZ2$W{75f)3wg *1;A펭YJcW`ߎ[-f=}oğ# cFͽC qޗ7Z mW)HI-;qf&? ywDw< Zb=*7G6j` YV,&߿>6A7Q.tZ8;4ftcC\;5wӒŒhg:_kdk6}/,Utvᐇ&UU%rn=YY3{Er8;odk-/μ.,&˒/?m=* {Ε4)"2-!^׈sZM-#Itc'Z?vZ+^JŏmXsxbRzӞ$bn: 贎/<&d̊m+|(@Uu~cpb?u'J7 gmwFCaZeBc$hmxR׉On{ ,O9 e@귚ྞw~^Xx՛}٫ћK8蹻NҕD@GT!͘mQa39+7B+Y(h71[yYl"e0k.'>q}gs"ggw_˵cn"{@?qZ3Oj?cbq0!~r:؛;t % .h.'m).pv^F\?`H-U/gTyz;ݑEZ?-~SSB|*UNkPΗ ('2hԿG,4:]ʪȯ巪ξĚj>[9gCa؟ye<0_UQYcOZrrmvc2q8RiKd=[ޔztfJj|sJ`2O?s,/x>~{x[_}Z\3{G~7؆$SO;s]6)[$hBFE3U[2ߕ]C]AΗG%~Pe$n1ר 7gU'р)M:I5*qhwg7kh~n{/2h]~tD츟5QK,#",vhZ*sU&&:SpaG4}T0JcM}$2?3E&Wޝ7j^~nlûzRsZE oAAwx'Mٖ1-%k 2"648M^BE UfeFmE7?Jɳvc>UO_=ހ5)_}I6yiS(`ө>'|`f. `*q0|?-o/߻NUIզGN' nP0o$0o,-y3, G=[da}=.5Îk'kC1)ZSDž};`PnƉmKS=VB-O*=ovߤZ0\׻lZ3Z0V/|b\Cu?޻Л2 g3̾ݻg"öbBizZ|tZXLuז X01|d [ʊ> l8玍kT0T skQ+"Rޡu?1Wl _=5skqYnW㼆X}W kyŒpH/yTuo;w<=*qs)SL8Q8>Y!YA![%a>Pv̄O5yy;[TCy~n9ޯ7l_Djп<6Kԫ2.[jyK N/CbhlJF}qלg{l7L~Hz;SĮMg3\9IgἨͼ>g&&SXvƳg y-7\ʾXnqAڬ3gӿKbDw hBo3l.?xUy(aS4IJCo*ehnϗ'l[h?Hy$1;kLJz֊k.e?!ם])g4ٝz3{[|+-ӏm33xۋzV3&p-%8r0*<7 U3OggS@N[2: ^k _{k=TFXB.wqڧ/[_}M6WS%q% h&+ctGSVj0iӏ_o&k-?k@~y Bӏhll1bϚ`~}NAtgԜ9iױ=^/926g9yGTu.r=O/!>߳M~ݻgMf9O"jBW僀c'6|#6$=jzD!+~vxw\ֺ/9mA!yΔg &5 @#|M2+f!-5)IZ{sϟ~}&H]m^ϞW$f)`fnq 9UO+m7C"95MasH} yg,\#"ytPx>0Tuk۷-<]*3.ܼۙa痹:ޤ_U[\ Yu}YVy?*~4e'_5U:`s/w23϶}[nvg/`(.̔q ɶﭜ׼kݰI<'qguL^m5^]E{2&,M3"Xٱk3]24U]y{/b\A4Oi w$pGcqzkf 2Fd3O3 V`ocO0$YeMkxJD`surׁK`jׇڤq={ނ&_1{|;&[RF52k^:Mƃ0i`.׈ϸ;gN3kYr5}hԛ?}i**d %"ܡ)I7(߉sT_)K%yŝ#& KøJ’ғgͬ=ק&hLyT5S8Ȳ~8,۷P>A=K~/ !|e>4!2Qs=D[7ڌg=W?t'EΜ}u›s!ӒۇO.-H٨UN~G?i~#,\"IX[iį2,ErrlaЖ(1hs_ 2^'/Pd"bGɯh 4<~RM/s:36 A I-jϳ\jJxPkc=k3)էB\rP&U×{_8!moJ%N0t7CȥgI~ϡ0-i.?MnJp^˝kC >_CfIecsFcݻa+;k=6@7oƍ]1 pԀSM=%U{ǩTZZc9l>Ӷ?5-kڢnjsMNfs;_l 0Sh7}~vV.D#:tײ.gufy[7=^W(VUo/5fC}mG=#]e[yVhY[Wd i{VO?NѿZ\9Ág~l>iY~z7gG?d^#v X -/ˎy{4Ǟ}pxL}[UbwXdz?(r-Pyk7wO3C˞>}+h;ۇzJ?έ0pm_j-!#콒8>$;.IP~Mh22+RX|ָثw }5 l@/ P @%cI 6W{?=LjG~5s9ݥIe>4[K@z@tEcq_;`{8dFc8sA8|ym9xJ~\zz3qH;qW)D2c@b~yUGbs=X˾܇9>\_L>LYY3Mf?yCP D~^w Ksli)I_5ߌl?a!Ӂcmo֫IY1ł:0ێqF"g㖥<ʙb x/X9;LT>~:VӗՇgu޽uTw.ރoiKliC9ģKUm^<;}!urh40*?mM FM͘5xnkW?G}[!W8t$lf&`/N'{ꬥ<{K:3ײC) m~~N ޙwGFFN)Jf5ۄ>V;;:x<~>iu@?YpKs(>bgrY:R'.r،|}?RR=t|ᾒ^Ats袋i'5\#;* Z5gG!"ΈKOqQ:rXw8LC_ #2yyϭxD6x}֡օ~ڠw@̐8J$/xIqoíGiQk]NQ l]&yOf=bfOzT`O[nZ}1sgcoi$C9{ӕ쉻Vgb}#p5d٘@>^zx^z{+`|w7n}w_[c8TS,DѶz|'a31Sn-ALf}8 칞Y'|ytYKWp gbo+.55~|5I.oI.wv.uߨ]E4C]^+nNݲWׁ͔myno.帮4;i&9/q*o>n]%'j&%۶L9OA2@dws)gR[Bqx=?PGqw&A4rZsus{Y/sL|@-q^z#=yg~mܪ:Pڭ n(רRA@.Akp=Jv] ubI*13b Fn ] zS~.GrVDl1I 7 \D=՜NF맱mAH]^}asxE$:1Eiy3K 1}=̮ι ?mӛ3]nVoޙ{H -h!>\rx5+Y;ɓKtg1c~mNʽVdZwSpw !_DSU,;/VjӵU5g}[[=KSzzFxkضXU }#nfq+I>?%IIrC&CٗmÚ/#zI!OggS@r[3ݲꭩn>(6i4?_~^9Ton@M0z5gl<=0 -}s;kPfjYd#6uMas'ˎ3woYe? r_^: ƶn#n ˍ|<z-l>GcDZU;KkrS\?!R ˎ:?&Є(D\cb{G~ט vI.Kl 6cKg)8>z/.SYsCmTsҲfld]}4ү|Ȟ]cL}e..Sj],6{g;_oe^Xy({mxA7;Ԇ.6w> SLm<#Jnlm6k|̰wP]w}<80뙰kH',\Me @x_{pѻo18~k6{Z*.b`0󬕭Ʒ'YPw@3̝Cs~=%É0҃גw} >Or_$I~~Эˠӓ[t&y[j->hO@@_h'vyнO!su+:cs_f)>Z$ [˅L#e>֒AogleyO?&{Ԛd66 䀈 e Ao5>5Wd)B\?c=bfޛ2t Sט1M%f斢Z{> m-T԰RDwh0J'g\e]=F.dq둵a]j=oy3ۼFc"T7,B4?#gW<6`XBXFùq}2lſ}as^BGqvꞘj.~Z?{<C}Y' .\zK|l^)3/\.}a[VLt?X>B~(\;rX^aLy@љe^a/Ӷx4FGKuuS.Oy(I==LbB5CJ׻}|h5ěүWL[4:S_†&2 ! 䖹_o &\CQJ=ob2=͕ښ4]9?mlIߍq@9RZs l^M@ӏ|[gP?|+H 2]|#>o 6UUГ$C yjN6~n*/f{3׸罧fr,*xxuNc˗=JdȵDS4`٫.A٩ 5>?朗MGu~چ+O_~6mo7UK.~Gb}8d~~Ɨb?̺/6\uSI uEG3pUWѼޞ5C>daY$?l8,27;lw/um9GgӦqOVL6-ڶ4#Hkkrįĺ} m>MQן-QzK6ZWX>juZc jbxucD!b)s,nvY,&+4tYGS$Ɂ릁cn=*yz(qGM 68kў!ǡ̟ xAtنrɰ sB>M3v=˘s]3Ζ8_=^c OgrLۻ8m7.sةr$E\ӒbK]k{`fyb.=_Gt~ߦ1{=Vbū?~-Ěݝ;)T|R^灙 Bc1ܻOO~麣8#gG~d_Ҩ6f.a=[Yz7?}9df-rQvsFvX| sb\ Ӌ~sˣ)]W{jXsEGuV9Ş?lB}gkzLf4{^U%.1Nd$lquQF_jWo_dw]yxNT]sge?NjLD&k5^y?P#-E;"s'6--{9p\4>S^^|<ꯆ䚙%E~fݾW_#=\zkT{ a3_j~z{~4ΩfZ/P\"%/.9LqC6"ٟNmӒ(,8MkcbC&LXD(7u5eAou>% Y>>*YŤu1a04g?1D??asqMz9hCQ}JO=I#lS"VTEkmI"BNs/@3O[z'l 4q=#- <\ݦEP2sDP~{qFzA m arS|' _-{bf[z N]7TMuIYDjM]aRW>{کjA4EI iy_!p,Y>欻\5Wu0W?{~B#wu|t?jVOo7.R}%/c"ٶ?'$"#FA?͓L-10j;)@O & ҚY^!wr&. v$ "P\֚_sj~>cReYd: =ЉR/,jlzs{Է'OYCeء݄SiM.r(N3H]uBݷ_tS/j͛mؿ%Dko[8`^9[:;ry5w1aR T^뷤3HrúGcgs_kv|>5@{TG*j%M0Ϋō{gnb5x9v@0>|?o LU^8,ֹ[]Gɸ_ߤsXӋ1CSjW ;+U3eB~:>^?4 I9:w\~4v0v#}ijGHp˷-@{eki;irsHK3lBOٹ4ǣ%^l>H[RoG9:5=nגq_ޗ/__Z;7) o7w=㞕;o"*=[o1d^'2T^kk^:~|{ї{y#;} +7ƺi9s٦\_ ĎokFu4w^.~h.K]?[_:|e|T8a[Q.>Ƒöhc%sa=Љ"D'#ȼ4x__S#q<:mX޾ Ml>!aިY_k8YA &s#,8Yut)'{F094"O 9Yb y8@24'xDP;_*-/;OggS@[4ϼߑ $[K{Zplk2}~ۋ,{^#{?nC]e н;eaH*K{up"?0}vUO!m5QrݽbX^},,QB&ю1ZZ,A`d^ƨ޵n5OzO,D ~<:˓GGy_I^:uYeҶ^JvL]/?[+et ֜F9s"~s9فˏ\~wju]X瀪f|I"&B7=yz<953߭WᅳdPN\2>$%;\?ܳf&; D9 b'#sOG=2?se *?9mt/, @q !kuwQfy=O=298DA@5!:$#_u^պCr񁄙$wHgo?d̮Cb|'f< zXqJoU.qHpb"2zUK/VUgs Wׯ:T,r»FK]Moy!Fna$6-P=kY~13f>o5rޯ?ܖ˗syfo֕z?yuLeey/U#B#}W- YCz\06a k'+sK?5_>柶j휽ޯ֟.f"})g.s'aatۖ'3r5Lٳ3ٲܓ8hy,{>t}h]n֓ǛI1Dd_FƉ'`8%f Ζy񶙐e>ݟJnex/~ɗWJ 0Wj'"m}zIJϱfYk+qB|ڣGL=dLR ħh 8>I>/50z+ /z_d|{jJڎЮs;œ.:I巎@HaN<|y7M5[>Vm!CA.1Sϛq#!y_uE|6Ɯ/ ='+" v6%NcŕHACu.(&vu>&uVA"k@ցnTZ9;lC]1}﯌7rdғI'–?6Z6&̺4|DGͿOQ׋Lp?VbCsy dY "DҊQszoկrAý;8  Ni NZJ$@כsr錆{nikʦ7Q=ojjwBt)6 lz[:J_:_O9泻0:{~;:CķNeYHo5yPleB)["Au5{aȼ!Oښ=nʍԊt7OkMum gs{M-7Wxb39D|&g>q-<~4b%ۋaӝfGlu\wvzmE)!BM%Q)g}GO"0c,bwyE9!N}8c{wgu"~|b8PCo@=;~k}NdG& nY4K䞤l΀?ʙx>=ȉ֓5UiӞ2A/ɷenGwLqN^:4wK1g-\҇+-i6 }_k]Ggs9V紭&=Mur.ű$"_ɌMv\\4qEHZ` _pD%|3;?n>I=Eiq S,0G]WN6]ŋ IRa)(;cMGaGٗ:䵢^Aȸ'&Vٗ6&[GFہ9y]3حM^'MEͣV}mP__æm9_2Y;.ߝIg\vy"aD0*>z79#]|L2)cYCr%h0{x8vL3VdCI-:ut9 o$Ihuc}uaMg `:MkU*$N˭Tvxh.o{l/)] hs]vRY ;l(4Ygd?j9&%W}?yQU kϑb0_.d'/,|k~L/y }*.7;:{K;md[_ٗvoWmVyw߾/سe|\;\{ig#1ֻI2x#[xXʻn5'XEg{~ðo=(sߑږp:>ǞcӉ({Yh^df;dw70kґIؿ 9:%N{޵RŲ-sxzFoµާsU4 쪘9 [ Ki|/s~zp~K 2=ޥEق9wK?ET,%OYGy CdP믷?G=Ouhrݏ=XOΕ;AEZ5c9xU9n3]fkܖCrEzץ#ìn`חc}ćz)t2-w$ⵤe(Ef\$iol9;Qkd'E猠'l2LUV^FWa0?=]x0ه`Z%)ݟ߫~!~ߎi-Kyۚjkp/P70T J\.3A;h!Rvۊ :Ƿ}~Fxo.s^B.Ɋ5J[(v 9J=lnS{~r,2툙p7CJCO=SR> MovXJ ˏ6g9a}0w=CmސA'C\.?KK4!umKĢw `POggS@[5 `/c཮D?q3}0n= &JQ}vՓ7V,$a#A䔜ݏdG%0`17qCdםߥn|+ը^n7tTOyF׉~dq`O~{AP3}O1Ay={qֈcmwyqE>W~e7ds0윋}׆l'5u}o"ub{WpA̜rPq.vʠ%X?M˥m{o` ?g^ʖA@v%@e~;$<-%h{{#u]e_y- cjkHk(eYgzxVdfzah LY)Lx2>/ꝢɒTf^~[~:enāGl$;)֕'X+c`1WPዕUqja{ž_iVտGlc޴W]Kd;_+1fΘь~ TUe:>55_'qe.a>xy\Eא"ȫt׶4k~k% ,^5ODs,g꾗G-O;r> bx6n/:%2=Kxns/Q(#SDîCc]Z,Kv|:^CY5!>3a?~'Qf&X8n.qe@]Ybl>13Z3gg3%Ozphך;; <.mWͲ_v[8Zer#'?` :.K=|h4smnpc7u-^5f+UNDOOgJGҠγ/d>|jS! |""4&~=2[~cŷ}Ǖٚ`Z1<{^;U$f @p1{m{?ufQjﳗ@-tprX{l"9[nզ\ԣ^ՒQ}7Z69^4uܯ+[ \|%5r Du2]0~FQCşBg kBݦw+?#E'E~Ϟj9mVdG(|]G4-Cm_ini2vqyoU.YOYv5V4^'G+"ʡaS<%F3BG󇪥Nv<^^.*j)d^?p *Mcj}noQqP&-}, >KyODG5WȜMy* [xNcx{f^oz?k{}h}4`>6Kl|iuyf1TX{}Dwt4J[Yonղ=BcKm泦0mu1{>io_?|&~EҮKѧh_~~9OqNйky_uyۙx{i;tx"x"O. 2x˽yIgiwC? Ż ofZ<^e2owrC ōfw^Cd^۳wzyKrj%H^5zm/G=Ύwe%{l\7Ξ-W춚H}ċL6)l^`5%uZTH>ЖM < }#3$vyI-?]5cO($e*>==JGɊCPJʭ*7>5B& ;3_giRSӼ4kF6C@{;@oNU%qI ID98+}nt%pԟk::5ns|ן6_z݂9Qh>j8Y7[9Qs @JtI-tYB󱣉s_:?γ_z ކ_6ۊs#ˎSĞ>rm\mz6Y=^ʟ'F(< X1{g ؔ}"LoIMVU;8cvhd>=zxj=MgG}yɲ '_ϳٳ4; JVs^Уu|tgyJٿ`c9:f;\u*5joD ٦1mՈ0 87IS7j K 0UpM=p_ H1zm6w˸y>{vSMܨrU$)Hb ÔtM-=rTl?){k^gq6k(䞡gׯfXp5]RE^f^O/N?ږ N,q:3 w;D}5uk?W5c׏ up7(4=m~lfe?6.ovvy~םT,) 3gITR!AЩ7fԿ[ޝޡVPOCJ%끄rNi A`bfS(}]=Hz}5[>g6 2ZR:K0kF<yu{ׇ3߳ջCof;\k1<\nvN=U#5&o ~ 9o`|nHbT8@;y 48 =#|S^_6D2zng6cχ̏]TzC8џK_>|~'}܏{Ooe5)_/?:HRAb<9d%Q.d}{k,OA_YOo;i2e!# 1rΉ9!:\)r~?IkWsA-Y)vl).d#ˋf܅jmG˹}'7GꝾ{)9GqJ;$mK'٫"h C/`.eExIf3ΈwYtkWAUd@OggS[6؝ UU\Z5nW<ܯahV7=^k\ь~z+}?7|yrGu']I,nyCI3fZ adRm8ún3/u75M4*gjޡ 6[mYVѥf喭1f~%4KgƺF~s\KoߪfAdfuGNK;|yzy9;r:c[>EIR.)k_:*l޶{g$烺X7 X% ۲omy˼DZU/9aν˥ g3 ϼ.K~Hao{5߅&a pbMC= tt'Ii &˧55t$-56dw*y(wUJ9?HM6?wJUs:,?U?C44'>sgu>M9Kv<\\T2it3pϪ\]C0bc~zw'2y_9o:W'#w em3κsogä|^>D2}ANl*Q ]QڍNMvjRZUggP^=*y_[]s%;kqnV l.Q$ *[qk4@A']iZyk^\Ywyx}fk&WC,??)=䡲\*E T\L*)In_`&-%Qy>{+kΗ:5'N_S6g!5g~-o9=8{y/ݽys T"dH$,7hXee+^Oݳ+#Ut53Ս]"Է>i1B/Th$qLx\=Lu{پiSI&/? SrNE?֧8yKw8cä:[焵)2ԘNJ)y*ӢV)p+hz۱ݟ=>Soqf]m7ߕm9u8${/ˤQ'ww?3Xt~Y&U8AHF[focustimerhq-FocusTimer-8581be2/data/sounds/clock.ogg000066400000000000000000005120251520625676500227010ustar00rootroot00000000000000OggSg(9DvorbisOggSg(v Ovorbis/Xiph.Org libVorbis I 20140122 (Turpakäräjiin)"ARTIST=cmorris035 of freesound.orgTITLE=Clock Ticking>COMMENTS=http://freesound.org/people/cmorris035/sounds/202932/vorbis)BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s}{ 9N(|`Mbְ{#`1=WBu/uk[ x|| s_ =s-uߟI'i/u:nsV|54HD'ܺr1}q(7.*e5K xe˅LYp Ni%`i_Z{'Bc4>_JrI`S;V|:h. 2»~N듩XE/kePh9m BSLG1մs%Ȥ\`Z8{q}fz{MfSk؁7ŶtE Bar+=$PR''OW֯T}4ĸVbգPGJglDG, ]VE%+ۙ:\c$"2ht[[4pÌhUSq(a0F~h~Kh v /a>S7Q'ivR_,Íڻݬ}ٙpz^"QӌBYuu(M벯.sY? ]\$m9k}p|eP6pho ѭ&YxA"n1ag|PFH[/z+ɤ 5TqQݯN@ؔI+7YlxcðA$\GHV=}MSx)8P̝< RعU!7.ؖgUbm4΀ؔa=杠ux]3FwI?Q7R/,,:> i( {2EpVaidŽ.}U>h.e>J*B]Y:nu4VJM1܎F:/VJy{ľ J4jmѾr~j,u)խIZL*/ٖT?>n4(aR}2Ӱ{eoB));NNwQu]6#Aғ % 6<3_.]l_9tV̾~/w]y._B ɜ_so96D)iX*NPZnG8l)n$ʧNڑuN$2*I 9`@ˌӔ*E*:@LD`O8{_jV٬,?o|9}?c9MT Ǒ&`9L ',)Ea`t*L+4tNI;? `& I7X0S?7%=ZW,;2^_\Td̐@b4 @'Dvkw;,!%L 1#T&H"JpL5LUP73uE9~ybY!j>j\aK:vl.[Z Z e?_q\VT˦2X`&$BMMsIJ{6Fx{k!M񷋵kFRPS?t-s˯|aY_2͊˶GuM2%ݥtXߒxW%JVK<PPv4dRZ_3wW.U` uo=ƒ1uܙS߫2f/Lt6ݻͧ'ҙ`^{C\nhCEWEbcQzU<_^!ְ,.y7Ф^OEV{2ikRUF~i}=qy^l)ק~+8nju,Pa8W)_okYlpk%=Z.\%t߆ܙ E/N׺jͦNK|[ޱ'Juc3ڹ7wvg?*n|bS%E\a=tl72շ4:[ҔlL94VԲ.wYΘ2\( (]}_Ciy@3Ү7ҽU⊇f~^⊎'2gFF'öJihuʂ($uL^u-7UOih`GQ.\ύ4'7:uBv^.i/eLO\_u󝥣A+\=.v]bAO_ݳa *5 t%A5ޞJW"FwܟɭW/^*oksP8?:ܛfXv"3),ڷDiL6$ԁ?ـN2 7lRS^d|+QќHMc0)m&|hh- oŅn;^7x9,J@Iz9t"BUUK1,bjdc]0Nsww2K0L+A<5,& yZ+iC| `:h1עE_lI ̸x#CřBb9`x)R`ذS;{acYVo ? }0V:BJY<`W:h1x=H"kZD1FJak13PA |:¸ ?SB7*Z3nqᾙ7p=w7E;,|) 2/5uѓ$1nsBBkŦd8ƔX-D\mj@/sUrWIf=ɎmG_]iU%UqG- h2%#MWB`m'Kl݈MԺHuW%N2=+D\3oo2!p1F w}s||Gdr6.sԢ||34b#2>À0({āɤb. B4Zx~1z˷c hBrKXeTd\1'F:& Sfe)D{YK&6IY ).|A EdRe[]+>bi0L{rϮ܏]uNc/zye03G\\5 ܖ`Q ; * vdLR OB$w|ꪐ?'EtS@,p+@k,+-yBqEoMd W- v̴Flijm2 F숇T!!>ʴT*664# HS#r~0-OaIa w?A$ak?jBk#Tv*!J OggS@g(N^|Œļ7&A]suaIM@cdԊ.\8|Ub,.<62ˣ3Mc`i4~ ['=)YɤG-УLwMǁc_@Y?=5# Z}fo$ŏ RR@CO@(?vIMi;l$!|nدoLzGz8 >?^% w+vfҪJc,uaG)27s##onz* z7իECp.lBM S&ens`/8w5jox!yqy-cb3:o [87Q=uBuE_EgqbL gd$*J]#>KX)`ws|EC hBҥlL%jy2'C&)?ΤD7@}lVj`N+APL]T*+"K` oOa׭z6\ZNi_̺7Hmp>'6qT SĀ|)h{\L\)ϯx Dۡ6Mu9Jc5^4jDfʇTkny0Nd[kIr-<5FI.u|7-zJoqJ<.@3B5qo8^$ fQ]W9ܒ7mS#Hi 51rEknɰ=NK|Km0^w+eе9$S`q[8!)%> lFDkI  $~D^VșrT{@C]Wp^ߦ3)iS[T?î-HE&Í;)^7_j3($$7^v9;83)s[?_ GB4`Ii2 РY^8]ֹY@P:Y4wa?^dk.nsbݿ`nҒb4(0VXJa9ElY0ol.48j-b[:k=MǙ[q&J8kZ2009 -ln8)eē[YLhb8}bk2^:PŽ-mzfc:tE5 ralUɩEtF݆`}J FE!֋"ۂNg~^,kS k=:eZsXRG!waEjJMJB$.1\G+;+CM;&jP8ұ4Q4"υfh k^ V>BHy$ix/\QX{Ǽ^{`UvuMEp %O׷K5jX[*鶷F&Կ7u}me6i7. 0[r9UY>K]ў 6k[:[)(skB&KW3'ڻ=/U*f|շ3zvW0(FȑB 28ҍ}ާ6Ps':IwrEDP.ԟ,,1t5T5s)bC9V#0IS11F[JPSO6 ׼o{A0{a L2}c:vNXۜ5mŚW?Z8S9LwI2aw[0ۯbT/_0D.[ӧMT{fquU\bnzuNR3yiO<뷳-pR1 fnߘpX[$5!MRU\w{I|Fd2#qXi/$ @;!JP3F-nUy^(L ZúnѪ[rw`N {@|@:$ʊebMzmٖgI)'D!PQ`i_lYl:brF#/tƭNc@Jp8}}SO/5uqF27  f놦( `!{[;fOs$@20d6;P<=uuaej_uf<_j*Hcz/LT׬~,:G>#+wvAFp7 `[` Y#ȉB8KD|o~2%!y$MJnDLCR-9iw^#t^^yFJ?֡&df 89m54MS)e+0ڏ5ݷzS [u !#Z_RA0/&V璤!0'%ND|RRT>|1K$'Mv>-G? [X.}&)ʴȔ0_. KgmGM]kO \zxęЕlu)܉c<=@7^QBl&uV$ռQgAgΊc D8䵤 Us^Fng{l{0xW4 ٰs@2m6RӍ{yoMʀTTy<`@YF餴4TSÿTU"QXpWZUO;} in1V4OT}{\TMc`Z9Ҕ3qm*mQ`du{u+{]{ozT2LRRz_;nД1Iy#^]gx}b딃6vm$I);kn?#*ѱ0(:xρ6H8g:3e,K9%UU(EYjD\J ڋ,. \ l5SdOqR:k@1B!]HB>wl q7AYGmR1WcAWߜ(آ;(z,t{'3yKբs +@y8Y391Y뵵dMI6T 'F 713pO.2f֑2w.-UkA귩l<u^C Y~TO!Gs c[n~\l<$|ԯ}cv K+8n S7`:Лi$x) ?s˝= XN#*drcc 2_Z~9Zd ȈU,4Վ;4@sSm'QmFm>,i;ǐdFKSzpQ/KM+EiV/3rw*%:LQXBU+%01yPᷟx-DQ/ʩ4à/sݞ| 8$lb>,N3d?k/ 9|(o_tv<(\aĈkdjxˑkEB⛁$GӤW>SX>ˑ(H].acSf:I}IBҀ8d\;exҐE\kmApu>9hY)PIjI&fD`ԩA$L!׃tkd v6wmѰ/&5J[\qF=ސY8sZظve*}wwO)0zu C!z5Gkܱ ~|^FH43 y=r϶-fh ǭCa"Xceŕ{{lc!ų5te>=NiݞFoz z^n1׃$a&ժzwU"_CFExeoC}fXR&%jk6~Yϯ Dw@&"\F iGGwI{֎WOڢwc+|WkHPCj[` @g_f0-NNH$(MA1; |{c׳mFђSfMb@ʂ{1V,2N;/Ю&}oV#l:a 4^X"P6B 0Y1i. Bڥ6ql2,y?1qclS?i5PܻBdkqa!oqѫjX`^|^۠ `yviVSiqX|"_o:(!xlx XFx{/HxFDYbrGi!nO\ Dwt|eHJnzdgVso]㼶t]mhn0.N1n#iǀ-EaJG5'yޚhbUOT!]Jf9z/$8״.i ^ЌWTcmI~0]Vu"/ֲ6!%ayf$e~nѧޫRê[^)()agEe1 ԛw,imkϬxws{#2r| 9 xY HL}h 8ZoxZ>^Vc|0_.,ș{KHm A.o`LjFS_~7hM>mEI4"kQͭ|=b.CLKct*)99Ym#23ڷxRгrȩ^ ogwUc%-Bt!Zu[ظ826Ŝ^pOߘP~|>&7 /4^zU@J iL;zjL͇%Ǐ-Ci}s "(3*:NuԀ17088rxD'<`8sCjn'RĦZNUqx*ciz:[^廹^[ qo4 B޴Dlup5[٩9G`{)Z4:JkK16˰4h~||6?# =0H ?"[01r`БGla'C&[Ҝ/_ w&ZGHm<`Q Iq^NR;*TN6UT},Gt..;ihrzrF}/ȭPMf5_Ī4[}"n3MeQ cL3=5Uʰwdx+-kᾒvat||IXMrB`e ARvxKg֞f;z'{žo۶54bJls2OdCf1=6SH[ ܅,4N*1 %p3fNy&n1(7,Hh}ůmeDE$ClCYk*vyA\5?RHMDDJvrSnSg`xzW4Ԩ6XӚvPHck. qG/ 'Ik8Nx(=8'TVpa׏3S_2,^Zv00ݠCA^F. ýY窓n}IiTU9z3Mr:rF{%v:D(^1wUo(H<& &˶hX[C l!"E5l$g%7pț MM,Fp&:$(8g{7B` @Zɛ%F_ 2~;rT%!njp &w2Ƶ>h:a'ݲ$(^<CK={ a5%u3}5Ux3kۇQ[]oGL!!sҲiu;nt#p~fEB9]|zCb%kR_q<^JrE38HRdH .S5pT9|?R ЭBiՍdK-nikؔwl-i{ʓ2}깔#HQ\P]}\1= E 驝t7rΨzN!ioQ-[fS(+ݾ&tjYK fu*RsOw[Ѡ^g0iİzCjflG#W*K\(K zXsroY\ӣһ]ȷKIOy!N"^h~bB 5{|ɉ4ZQd%̬z4^e[[7O#]D+#7Z]2]F.w*e-=wM9Mk>L*@rnX5, =Ls:F+{?:w+/RM/TT[xv /x?::ԧˮ(d%BCu+ݳ ;~^7'istM?%h/~,d9I!alhjVx{׿-7K;9-L>_qgS:Yz.K~!45֏];!:@s&G/U4&ޏ[MY8 {sLZ)+IR7'jy[3z񚰯WLuvxmB⣴pX⸕Daq !&$n,! ֯˲FgY'%\ݾϼwޜ2}}u]sPZ\k] qP*>N=]mꭴqiz,cƹp7vyF.|s?{k*P!ȌM3X~jt1}z]H/ngK[r΃sͣ|.3@]Dh 2Yٌ>4`Zx+=vG"ڽc㏟O>Z8}N긓%=g7Gz֍KpobtGl{$r{)JזVW^SV'c'4H^h=9fbkr.EV[s%et?dN*[!'Kb۷F_=LtkcUԶ_,{oQIJx&TƯMtzꃆyujֺx{dybRv블!xvwZe}BnǙ ͕C5[)uM6 &uYy}`6|sO:~VշW;ɿuG'G;VP e>Uj B0TErTedLnMDl?^|NօƩk~ax{ǩyNW_ %=  Udb L3\fd_[gf:^KOyqyt O+،mQ25eߒ6D[GÍ03 wdb)@"ء(ujYPBfpF!f`=9T_ 1XI\:.tP|q F>^6 WP|6 { `BSꃟhTҧL!rX@ 3g@)|p"+ҕ!' R^X Yf,$}s9!lBuZzt %8W?F7lls^{̇vN}Ed.}{9`w{<.:GŽY6Mfk<~ ЍܘN7)Z艨IA @Zr:b6 ?N8v\M՝'i(81LjȐ>2Ru_ e!ݾO4 iYQc 0^? +Q @nrgT|^vOO $K*A}Z]H2N+3eKݔqF%}4Bؖ_*><˜%v=l<@ 8EDX$61?kKC~BlYSMa)\Eq@ADQ"B-bP+`הZn v=\Ѵ(j+0{9 sr}#p?R%G>S682Hh.V*oܡwS ^"‚My@CMT^l1{zS,<=zs4Bƚk~|,chAf~dJ&8QGn"IdֆZTV[ GE;@r v+pR"cRLty 3>F7#7n`י=jֈz Ү`"=nQ-~<&MYb&>n A2#Э}POq~!5)(@$@ɡȝ`o\`p1cKET^.cB $[a0DLQ<<ڋ5E]}Qk/ ֆV롅!QL@nOEx`:1t LL'3VxC%AJ}%rW-S2y@XRqtD0SUWv!ޞe͝JWB@@fxvX Ccd Y;^=E^`Ј܈!æhɉU7Y蝟 G_mS v%d%F&&D^Jq?AYuQc9 SM.Ӏ= yӽj9q:;Ψ-tPL-@`1':$x`(r?ORJ%|]zRn,?*ͯKǍL,eN@? 0jgL6d4퐟Evz2C$a72O^)),MPH2hJ}&-Jte-AQ5*">#`Ȭ?M[4,Qltq)[JYV-)yDRm(*|^F 6&$ ?<,upUoHm}=/T|wK5̽½(PgO$[uEt7Qɵ۵#ǶV_$~bO2j=7NAz<ۦgaQN7z; ])f7?[ 8e [9q.N^ .}}8zz"ˉIqi|{o"W}vYpݺHqĿ[ղW+߾&Xj]*l:K8詖]5 і.cE٬veE ֖ ٨9&.*Tl$xč(Eݲ%m|rFy ( #%x6-ud.[1X&uF?=qn.'&8W.6(޸޷|J-ޫcuIP$Ls&p)xtkc) "leDwZ* `]XLxU@TjвT:)y-U6?).³n_-LBPЬEO Kt"%fFx?ynWsl2uֹֽ{`w֟W" F-(@ RM >]'8hQF&`QV4h`6\UwY0 p曙ymSr_YRAp`@_|XYt6bf=t K%!h'oPj-8>r! :tyƮe}$QwMwb+OIި{hph'>>NV|B3}s.9H3 růrAW/"nm]I4Z5vudfrX+?\x!oJf}BZ&1@?ֻf9@Z!v OggS@Yg(U^|ZIv2&xQ@6}#iZ&2_9P&ujaLR> ob y[88r.Jgk|xmE4qɽw4)EeO*=i.T-V"\%pظ&&e ժZ¤c>YF2'oHu^ѕf|{l{5oJm*_K;>F]i/T5PPTpR]1Ӥ&{@-# Y<^#3dk"OxRY\e0tZm?)JuvZ$Jz%~!10~x>En0xz4FdCCi,4ٺ4bU#;D+;Ra.W3 5GsS [l?1iUvtj}_@Тme S]kYY GQX$7XǾ($WTdG>|[r Mp|u(n4z/wPHvxS'-<37EU@M;ϖSykZaDZοh XjIC#42~u:}GpwUъNޢQD"ȦzyZY5-}3M7+w;毼r/bm{7ǚ-4T:i~sТ{FYCnD˖"wz"ާN71Z0 >|4ЄNl:ڏ0Y `k+eݩ7.wv4g3).7;9B(W ŇJ>d+ "ǸQsv[#30?+U9l'ks>srಏD`KBFRk{X3ER+N xC]BdwC#U0+Nżb<9-᝻>|;)K?(man$@*n6o)|D-mfCn$ԭL^XWCT;kIXwt `QJ7Iƫܜcۉ9HgbD蘇bՃ"5ی )g,M^gz6}6@ ѶI-ֺ nEi+uiǠ}4 jfz`|$MEQ70$UkB5gu/ %$!{}Fՠ6|}]{$ 0u-E.S sT-zW5qcAGI+Qţm:CNeaD6ϰ  IN e|x)OX J̙xR46ۼlLhЕ{#N$J (ڽFiv'W715<9ܻwPN.x- śV0A;@~|\'H&':@yyx̚B}L͝z sUݢv*1"B n֍jVl\% p+띋w.Eb??\5Q62z(K"# dZ> ud - "36LW5h[Q3_.~)#PXs=(p&IGV#2,/0 vdg@fd`p%K}/fa6;3=+|TyN`G$8Ύ|]!ʥ, pWꛊ2S/幄ΰ'Jߟ{V7&bEU'gr=, kH÷3/v-$Aeb} f;/[ H2n:!E2sw)?dP;B7iz:t[^j#8V|߻ã{bMK|ԔuOѣ"y ZHsr- n'LhGkӄ>I&d0\~y ##"KifNM鵩͓ ɎҞZa<[ kVFwDle! {g2,! 7{hX;o2` BydD./lk.e˖q N(fcsbT8^|]ZInx?=HdJ> ( @Ek6l5+"{mqqmDhnVSݟSE"ͬci' 3?1SCv_Ƹ04Rm2Tb s;kpa@3l(q)q %%U>qNs2vjNy{jϼv]"~|K2VyRJH:z$g$vM-QNg: 㸝iAFKђD 2Ӎ.޻9L~(qyKZQYgnm MY62,;)鷉qh@+>JOFJ꼧:r.,A-˸twuOL$EAX^q"ލ ͠]X! Ȯ?֓6n\ݾ{ r|ݖҔkSi+&g̓S("9[ ˨ Y~ؼ=Fx?TD\o -P)2ik'SQys?0,*3G:clsn/ YTΣzXlçKpN4ttJ$uȅ6glXخ*Gx7~.sU_2_ bs5ϳѪ k[fS;~l_-A6:_-bM$\:0Y?z$e0l$:x$`@n2uG 0pKTydEBa|SC/mޓ4pV\P+0qL$wJku>j` P>95 \cjS R@Y\' (S8M"0LpuT3է L>r^r%6 r)hW*<{AL;^?*Ļ!C @~|Ky xޯu$-# f%#w-NPshws(,`G+>!R둽D`fM[QzϧWڽFmk~n'9I꤯_8 Q#?R͏m(3 M)޷򦌹K[XQH_W^i=`ek|ުX۹˝-(ާ|`!&AQ@"l%vNza[aUYՃޅ}X[2-d5)De^D]"+a鎭[W7EuYEu2检Ǽ8b> q뫂jxi:qlƬ"kJ*/WH<ι M O颦.ţH%LBsY#h(Dwm va#u]&>l[ PBxq7( z۹ @a+78i2X=*a Ə UO)K^ɃbzFVJ͐s`#]/D΋_4y찹}5iH܎A I`$lY#1H"sYӇM, |=ntY3[uͧ0۱tONX`K&zcwt{6-Bcr|[x7n ʺ4b@  94 礟y㼖$Fiŋ:3&jTӽ J2FtS-ymk1 Z,> Hh[Ep-ˢ/ky\x[Je`9Pw*wG_|}zk~o+EMѶQm3^)u&P(˦<, ߛ(ܛ.aiRy C LYWZHTethdW]OeFÅ!#uz]7|r= {Bҫ OK-F4U6PuU:4yc-'i_V[2dʝA= 1xuD2=fݜ]tgus44dd߀$wEL𴵋+YSEiRrzDžZ<-9*Ï3 S!NX;p!dbw:foj\Y DJg!D|Ψ^q@&'5 @xYcgjuwO t Ii2@8Q.RaQEkf Bv +-&$B@hs`Tf&򌏠I$ap  La4i\S8e GPNkL4}I HRk?H<7Gn[.zϼ %"UN (hK+0ՏgzWN ^ß}od ,!Y5~m[|),CQ PEPeJG[r=}v8<*:35Z~b;3=\Z=}³3T7t@eK`lj.P8ϲ-;rg:gGZJҨѥn!nG~'zh}iA505tlҽw}ޱgJǣ)+.wW;[& %E؅WnXD~EWq]<3?}_crUHolq\jN9!˞©^Q֎`h_ d:N}N>|Utk_AǞV[n:WKїLݭ3T9wngʇ/s){wJ*PmecC_?~UoC,{w[,?%\km= "&Oi^ßGJSxh_? ,+ֺU_d|C˛I+p%}pxw:7khQ>_$SOc9~E \7:Yܫwg]*bQkHa!{kZP)6r普 .<s+[WU?ϡU@&VL_FRZ,*5 K(쏪҆_Q'ZjY]Ę$F l?cw>!W֕^l{>Lzp㟭׸ýUUcES846uTn?(D-呑ZTz"QJd|=2wUɪvmOzN?[߿;Kfֱ[G+'g6] .BtGtl~[Y,v,o57} ;ݛuQǞy镣{o+ $&iɦc?s{Qi_l}dr~;1> sd^p/o3{ yܹ>4JJn/y?OB"Ģe(MiTi'p$2RP󏆸?^i1 OggSg(s^!vZDnG 1ϳI&8\WdLJpF#Prtcm'ɵ)cen"xXH}t7\+Fܙ;֨XnPv@?l7Pǐ%ڠO5349_l[芣4CTyk+/ Q_ ez:yZFXq7ܓeM_~l} %S,^&%HH77'*L,ʶa(з=c Nˮ.ԝHUᠫ'Uc3rrU& Bmqs Cfb~J؂r^D66_'[ *iY~u`-k]U0YO$H.}u/(̭62ҸMӼyKd9~nsLT=ߡ2p^~sL 5pzk>@ms ^il0፻} Y梇=5 uXHVQJ///f%YݸlhP(<W-&.)s#_}&&>u2+j@ @=*&P3HʼXt#:fݻ m}_#]xEW1NUEr% ]px bkrƇ =(A7xMs E(5k Cu|CEolJތ@V ^) 0JյE pS$Kf ˆȮB:oK筥ctN.%C٠- _FYJwƟX,V|>ZFah)hmObT )qi8^\ϷtAG׃!4FRXO3' Km>|QZ *Ķ;5d@vGZܡ8 '.cU,R [w4S9SDg^FH~4}2pJ]Q]598h 4׽I7-d2]V>0+(|v0[kЫtuK@d%޻;1&4=̶3[!$N1@D fFS)oE0 ;%`[ v|!SFQ~C⼹̺aswI"Z#BXB:{@`JN;\ykGU'Ag 'o( }39*^|K3 АhqyJJ1w@OQUe**>wY ԇ[< *`g@-scեAe L2fɄ}G0@n$'"D $@ۍ /Ȥɠ. Ī*YS"\CݙxZ)Msuk{W?AI眗 R*`|a!t#~UI \[e!,P!ݾog]dGqy+A̒U),׹N}p?gm:Jt/i'bdb33Ֆdj14ͱ%{XBT%)r*ba'#i7$N7?w:VR xk f/sйVhx3mMpB^:g||wnx)g) n$nFY8Ui_pIV8cpM 5![(|߻< Zww@7z^|^["xv452EP72ɤ.+?iKV2|˿VzC>X;JxVSɜHUISc 8&[y\cФvVšWJ4Dmdϼ}>evP*k^SHWU G;:hW]~|ke8{gwfkd]P޶n["EG[gLKA鐻mʼv&_UjB^StktMСkvV.\+86ᥕŐٍ0esSZ v ݨUwE'8"F7v0Qj@ؾD~}uHۢ[|8ok ee!)EGw}icvr OggS/g( ReB^qEFb!>ퟙu٫ܳSo=}D`mJg(^̀jnkIOC$675'Mlp $܇M`%S.Au҄Ұ^gE,E5Zy'r\] 1.IR"Ǖkvx..HЬk자{q@(s2H(=1ʟ0rl9(=hR}?3I^jvϚ26XA.d0ǠY;Rv >Wh't;pkyxKA@ܑvb26Pfn:oTXS҆Xu:j\fκ^ҙ5an";:ź;zB9r* vyVSٻwj2NfY&A<.Pֹ̤beZ?u~65_`/=Vc>83{1h' 'l^41Dx{AIC&|䚁ZI.RQ#T+xgtIP4h!,^FIC țJ_ Fp<4i5pWz-I|V4DwJJ"nm8 izw {:$DO͑[)Ҵug jp#;d9 (S6f5~\]|\*oљ fP~LZ Ol[݂g\]eΖϭ,AbPd?`L|Ac (5y".tc <Ƭy"U M vp06MXOE(L>3c{4V:s-j f?8-['`eSLW|6z,Q%i"k&ʓ8=7hl(4ņ'ɝVd(ҥa$tI5$ȧ{uV,{Xo3F(!Csn+Q%5w38<"2QE diTPJ[3 |\Z h{$ @),p{j{YQ{kkC3k>jB3u({LZC6 E d /4㕍@"sGn j,FJ_/u~N}_i6.D ;D ~)\i 𔢊Wn΁p:[BujLYǼ$ '%ҙ+(ϘPexМ?/y󳷖ͽ%/<&0;с+}B)j4gnkO}Z5ڬ敉\YFkʷyp]#Ȭ)CbWK΋%|kpToKuSq2C5[#4c<†o+-ɻUH5N|27-I Z׼NAr  ~<#~^SJ@3J "YW&3[.O\96GcKn\[e"~n+Hu\M(z-*h!nE\8H*\.Bkgُ(8O'žb~%<\ÜGffqf%[yOEݛXNz8fjpIb}h92P=rggɑftD @ am,!s\Df"wOHʅ)vrܮcqR~ai$ f lnxaAQ;:=cLˑDє!x-%lV#˰e FfXg$j78dWtQwoĨؽƶѬ}u̮J1GiKdBY5 ꣷ 3+jA+p K=\qC:)rF](5@8S/aI fێwq` 0xLbZC\fElq7/<ӈq38sd]ȥI'DKT{43l6p536rgm9(8P$> vc'b-hۿjOVta[3Mk$:ܦ JvDl F6`m5_B:+eR#y[oˢ o~-8 :ovE)Mpc4<<߲+ );T39=T馢aE}j"뾠kɌ7x{ D>A&s`BOgFcfFF59s\4 qD)6- |RsQ\P
1hqy+|\K)dx}DoI؟e$iMv7OdХ X@9Ua!G jUh ۉ=.GzOF+yEfc6hNNnXجFxZ FNh^55fIФ躓&HUf{Rڗ Vy O!=Fzy)@rL|!2M*.O!&r`OggS@og( UVJLGGKLIHFKJHGHGާ%9OW$}`ҏDCI`)W04kVh'dN.mk'qrRs i¬ wNcyv%igĊI(lת8`pp_qK2#ߞ4$,pjd `mR[j#훰 rY_kkl_V*5[ }4:'9O+vѕ:^ؼ_[LD"S:$6F]L%;`|jkM~EZ"/щjeM 7)؝gJ6inj\+; u8":d$c_G=/gQ\2D,`a9$q\-[%oroDI1H]QV\~ 鯼Ivzbh:^Dy$) [x: 9m9J#mU\٣ ɬViͧ--&$q#2ڏDrul,Pe@+ҳ}y] (=WZS [Glb'RH@麍OPqM>S棈YO͢}v?gnpgg8>T'''ym{42:E~nRZc/:2+kT[i® G.^kS(4>ڬSq}ԙQ^RrWiޠl_Y/?p@]iPF[C4Q[Fk@rG!S I2ZWfKȲ8Q9 m,{umcrN:yESh i60L^<%4P7$@}?J٤yP5%k t^qWW|F0as#4!ھ땑54dr6f%&yK A,W`#SFZX" v;{BKN~O(D)a4`ӣ9B}qSz\Ϣ%D BaLQ'a?;mLvl9?0Џ5@K\IV&C)a5~- ,yYGe1A PFAP>1̐& Bi"IciSP}d+^nSk ixOv<@AR40T?ƺt-+*Øx; 2O5'] ʥ!_q᩵~|a]-3d Ό![MS:!AcҔeKhqhk) Po;ϖߚޝۻ?sg[ѹ75+s+ZrqԈMZ]!OyWgU&]quO˻fL{>ޥ*l/wfy1V~Wτ6!5HLޟh8,6}E zH'x֧OS+q} پ2Iny9ڿG[! 0t:tG{VXJBý\cݴ=(i;u+uzp0)No~Hz¨*#d!.iKъgyaTQFk(v#ّN/Й0Ք8}s{]gdGU*.h<S1Z&]x/m<|~b@@;y%wE:p&߯ Q,t-Inkd4$gu\U;d-~y9 ^xÙsn(9@>4Ә-6չ3 M#gQ%IRm0 z=yt b:qtec6-aLŸllt+^< ާfoUK> m{ؚ }Ϗf?(;okT;SΕ ZQe2@|Z|4bAı yTUWPWb+KBO47\>ݽ3яqK^K9"r*:! aiaڬ8w.B:&Ӯ`^g;ȮJVS;.R&,۷OTty]Pfa\ ~8yB[pbHj=粔B1xkK,ΙQ8#tٰZxAݲ3e,GJ˹̯.7483aYEcJo~=!o$MduyG\ ʻ 7̍2Ob&~؅43}4 P y`Kr)ޡ~|S\Aܨxt#C;os/_s)9ӕX`f*[Zj@!!]+ݮ">IBd4is%+Ai~m#5w?ub?TZJp؜d-P5n9cNj2 G5JvmKYR6 }'C2[8áf;·A_><Wȓ@0P:05?”̊ o[B>d"jKc_ m {dmyQ4Puzp[bY M;"s*iInup8R%ago0)s(RLF=gFN vjN&y#/Kxmv/QH̢gCPuǴ%#[:(!p'Ys]b5 { ~|- 4Xq8$fzNB#E `Mkuqp9}S6ALTij6bqdfS E=3㋉R=B@bXϮctm$nȔ똌KQ_kU+T)R]+Η~MY*+E[yg|Ų/G v*58iUD—8Q\+g΍F; ɾp"-"\B#؊r<av\`g5l'iR6azh8[3}]Yю)F:xؔz?RV\k\~hvD?GMe̪TF=|pZG[-֚[Qd$&\h.Kw\K! pGBP|70?NO_R|-dU%`-`V `֑tBKY߈K΍; gYX렩׀Ҟ3uSM9#ƞ 1A:6Oog2Jp b&C[Hk^7pg!MO0ihcHG'Ck u]YwbHGORN^/~|3k!@_ub%ϒ}z͔XwZF:phHnmjt }>s;۠i)ux[j)ɵo x#G32]iF=廣5d-_LTu+<6KV->NWqO,LJ oޜ"ȤI w! $qRk̦ޚ3ij9[vRo(C$_ \AbưuQ$K)0oMpp2yJZ_9. ,NqlZB6#,f7:9tSXP ǽK):h|wC`z8+l]պKB,5O ;w6Ƕ|QZNh_˓yўLV!q]3nP_2LЉ8~|^` $G@^$8ki #hodYe }m^Cڂc }|X)J7l HMVvlN/|4j&9-`E?u)k}_[A`RkIQ2)0O7S JV{AVQVش=3pz|їGV 7=zV0UhnHtsgYB' ؁]{DMJ>թ; .bjP+Yg ,?5CcQz$E#Vg|\w>$InCмf9yP#zYϻ0j薴+wD$J]']B + dtq: }T_4>ܖ#~F Q$&NILdInhΜTRl9ʊFOe᧒ ~k|ٟE΢^`\w|l/SzwvMSLE41@Kqqe]A,д6ɕS+ʻ4v4`si $PGG.i4J3|> !nGm,H`v~U]fW2K~3_6:0)j}HvMKӢak12ęHc|#?q7J9!1g$RJzuvtm@,U;lg.0I]i@9+2?4^](==Qb^G?Q~gjcPCn%>|K;f@Txw ak ,ɬ(+ڑ/ǻ^G &г%;@ M"%[b{lWDyV?G`GV}S Q2R9Ml+'2؄,u`1ݒGɽ7j% rNɰ D*(@%Cӄz/jJ|%^CPp-DzOrn(d%ăb\ o cZ@#Vуȉ#H6">ܾ&OA]6I=BFT"K/#޾=&vgOn]nH`cèc@_"ѡ7&F+^QO+ l3B >gFŜZ 2~/3lAer*us&uA3N~=_:k}5`.6dz֑ `0 Z .2xyNC.S?hf`Ivy-7.xjJ5BXF?D+Y3-:-ֈdj` -o0R^3K.-Hs c"/@f= b<o^& K^fo"ޢΠ_dw;㤔+gzA ,L0AyxY1n/:plx̏{N6'zMGUI&`' >zQ5*eROh TQsUOhfZ22N\yzIּ3"C˩qܧ>_BZze ]m M\Z.M C8LozW,/er9=ݷ!ѷ)nlĔ-p`ջz&)f̭;Pey/OggS g( M&'*))('FJ|DAv8 d6$(V3\0)xڱ4EN` ,Ll3|=bvtwx0sRs 16I6`+^9ǰ}"۟k_̉ )ݩNUNnXk&Z ȥ+Qs :geD8Ja2W'֨NLoXu-S>Zqw7`^|6'@$6d?NO\b_r3jAV/3{i'"HṶk8J@U\;s5CAS)l]CQmq7QUbpq98xxz@:ZeE̜5fd|s~J W60azɠouЪږI`0- >ER2'5.n^{-cݍl;{+r':]xZadNZGf>ݖ%f3|jʧxz4Zޕȹ{T288p 2.L7r >_IhԹh20 ȸ$e{;g[ : lEYrљJbGk}OF_QwfR> MO)}+hwCϴ'sPS$]>5!XQ4!z1 Ll5A-⯲nGlA`@BBw!><ʊ[(W>ܠsklz5u58.{i@F|4G=:/YD=AQJ U:٤ ^|=Z9Vo[x /r/B$%k `WKJqR@}qaS5l1OG?jvO߂t]{~a za}ϡo&IWR#{cE8eHG4{rzu L3O:y5-{r 0]5ЬH $2/vs]0{Iʎނs^b]Hc2u_+`{"anLy^[x?8,HGlr`=?$ޞWAG#Sϧ~Djf_p| bJRƆ^[!2Iά09RیJRYM~#i\G=ƒr)vS]lTm~ߚӣÔ*K֮/|!mdD>Fy8Q=bvm|Ruec{a `ЋH,eF pb!ͷFUCtGSF _8?&pi$ev9 I5\%==O+.Ri:()~ooy*1nHLFj i82a>%ջ3ZnMnt^Dt^",Pus̐?)h(ijGkMҜ ~<cs¦cqi , |c1El ȄI2ކ·>3dpJʸ;_G%au%S!RL!&vmPVlxSo@}Uݲ%eic %b8JBxZ31Wj[w ;0ikj?(E(=삧YggA #=Wߦr}˜?lH2a8b( )GwQC|Cw2M@Js FwqXX#Xlf}y<]M$ր§]V0{JlB{L;f*<2FƋDr#giv6}N*í' 6 :R'ׁ:x|;Iy=gW_VufS52s4&(_>XT8#lŤbo`KUEhGqb,dn:[`78m0D&S$Fk@P_S)џ2~YajFל002Z#xi:i2=SMZj6-=R~\\ާRKō~;r L2HHh4{Z'Y Ip}-0\xϟ-uo ~``@ λŤd <- Aݖf9QT#y#2zyi^Ꮈv+J*7sViX[OD4Yn.Z`2ĔFBǏɈ@;̜CRhbMA+.Θ5r]a Rd+aX%o)9`>$Έ҈׳X }3W7gf(ďrg$ ~|*a HlkHyȒtEw(|8ͻ^N.r O´$-,|8VCkqcm3(,32fN I38"&:<O|%4Dʬ3\`r_ Z&"1Q%2! ׭fA0|ֳf60TD9\>H\z3mM{hkL*P׌#j}RE^\}(y^hZ[π<4ݱׅv7N|N8dRǁOHA~-jDp|w+C'ÀPץS}ΛgIbxfp3RQ 1i__5%:'TT4>3i:ǨBBoR\]~=Xl֎}b_=Nzc+\t)ChPDsL8v,'@vd]ZVjyyrlyhlN֑G:ҼIyS=+vۣ*GMvHT OΌB H'`8&zg+woNYNab*a=s>B3ڸ4m9Ui$ީխ>âNرSOSWFl/?#D6S;0%[܀IԪ|8dFkٍSf6yc-gF2yq D7 93q:.Syt\ic%qNvܽtMb3kwEuJFGEbUt!TA?N@1ryn?Td{ +IJR'[v+qpdW>̕c3Z @1XM) ゥ?ƶ @T;h +wc DHfzI 7LE%Gqߞ5gkbDfSs~=/~lֽdHg &v5'LN С՟zIi(r)Y;9p ~y\3s|zMudOggSMg( ζIIGLJHI*(IFFA 7⹔NnlmRhFy&^w^G.y9Vy;<8xӗY}!u|au~_cl$ w$|~ڱ?/ukKnw'gO叉yҩۛ01%,L|5Ӎbs-e څ{}ח|J{bS/YV~o6Z$d=CyL˿ `L9]\4ýc妖$;Z.o6kO;j;{5o&i3+9 7uM3@,xICJjy? XuUz1VNmۭyGe^y.%uYϾJRjЊSY?s1|raWs͎;EH 2jDac-A6~4, lg<F,gUYtL~{^\1v=Ԋ,&L1VRqBQVv]גi'eOoұ@YH!_G [/1^@Æ;Ң7<>'Ի{/x.; ,J`>\ہ da֠^R^_ͽ'Q߻j+;ZhN$I8#$=i!*,ZVLd܊x/༈lf<b~\vu 3L.Ϗ!/caW^ 3C3-UySyi7Yny ^/iLOOi?az3)g02@>4!D=¹C.WeI05` `fBv=^}tݾn/q8,46QJt>՝bǏ%ZDZ1$9?]f8ypU! -` RX,&un/z$nPR Mೆ+,b\[½ps~7vb7y ճW|㘛Yi©0$lhbBCؘW@G`g4mz3@nr_2١ *2r"H3[ p6g͘0C/ϔXg)Z/?:HLQh?nD h*S/zx9SEi9`_,.9[Fnmi>Ԫ }Rvv1"d}w2Zƥ=O pO[^K{9`"DbN`Igc+c8KC5x:1YF89VS)@& c\s=C~8kqK|13q!nX՟b dX'P3N4'I]BW\mSr5<5p3ۊ:_/k% e[jB|3~`o:sԁqoD(imknm$\:O-[yQAWxF >P룞w@]#(CÊ@~RFnr~HB?"H˪Q q4Cdkr,~/x+5m=4|,yUm7o'zpyUK^T[S dž72K>;Іg8`L IBTotNg WUpzjF0 Q-2~T)={ez5hG> 0.Cq Ln$ͰȾM)kHMqZrQr HYi̠5cJfyndv`kfmrЊòΩl}8^#aqrݑތ׍Q:| 4} ﳋ (򼂩 6eX5{qr&J6Ce 3 ҾҐ(!R7I4\򩿔?Q^]w9BFfHJPPlyۍckJe346#H_}? r9R(`QڼX"Ί,Bч2R?Ѻ[|=щ"^D v@QbX) lث/Z4urS$ Hr?#|1>Om@y-|!?]uߡ"Oά^L7:ԊkD,?$EH⭳|6e;(!}GbE4Krxoo&*KD1TɘH[l\`7ӘUJO\ߪ$)lxIjtIw`2~i6ZNoMz-ΥD iI,Ar hE6-.eb η|@ A Ԍ757"j%­nM!ff)͉ xlFֺ !S}luʠzn2o(\&U$,yTw&Z/h[w',*{g5~ FGʣO',^>5WX޿d&f:?hf$݈eBϚ{((mSiR&kڪc4`.^zk?C:bB.ߊ[р~H=\F&tws$ C|jsÈ[`-yzm`=XY 8yG\I[3%>=dM:EhyZO"MTOwhOru-SPM\v(ṣo_00]{p~CmdȺ aݡ`qYRش%ZVD]GDH~|\$ &oIz}778EE#?yJj:/.Ӗ#dHG~.F(P]S&Og4]@Z=ĉ,=$>$vFQ窳fTYZY#%V6t9s}8Z6Z~5 U5ja&ZK6Y W ԃ=3_qcU}b|^G$&ameqE&S`dKF$A#j^ŅSR7,_ y10kns ^S\5!S{j#wg[znp%@zNWFzϝ}2rXWfkpOj<|`Px $"nlޅ!$v,N9Un]YU1}iML2_3B{bUE?֕C&3,,}9uTWd9HI ˟&F![g]+d>>y?28:xWgH L& 4y6{˔V}=ZE=$uFicHj8!N۴Ē Ў{ѵ 弶H|C 2`$"I)EqF Rj!zړ^vu:2ȷ^lQFim Z1jUK=߾\|e?XƑ84f)}POggSg(/Ҿ|vY,|^G e<\If?@xlgԽţY18۠zaRPB-(cx[$[~r ݗ~k[‚͈NPsU2Zb<)_6n a_8OkM˰8Ӂ(Z?LI1K]s@(Yt^edzfBc{r4&hk{[iGwxf J/SϣDrgRUPD ٰ\kZتh5N%f)Xq(~4-;/7~|z;nw>6i,+4EJd VOL{u}W3,`1y9'<.Nu j38ULRs6l'N:rqb7ƈP"Nf䬈([S=4@P9n˜ffL'LR×POuH{JP`Js0^ 6O 8@EyM KG۹rEee@$+i1e`5/j"_Pȶ5Ed^p'TT%aK{7 G. kbEj-6>6Y(ZPM)LSP[{ *TMX U1",BptQ$t@tUcNзm,~pd*1DM=: ~|[M؀72zĕIsf |]ت丹N$FiX; nC;sA6T<߶ͅn;vN:YK۟;.ڠdu h5-puC +Wf,qϻV `?ܗ~ X3 +1gUYhРHj8r2dHdt#_gʘRzv3оlu)-r e=F!y'A2:cٵ2Xc;w$ʁrN;'bA썿>|;2e0I$@$:$0p? ~@* Bܠ&g ;d9eQCtIZ1י)JD-<Ӕ]j::grum,576Ű^~30&)ŲVੑG`4= DL0n_T9,f1/*Et5y[j$4K[nX#8~|?j69P@Y'!I6h@4T|\ͳb{Vz|NM ˤH)]ܣ̡0.1z{fe"w0MQv;ޱ! a@+9(O<pyO@@^Z&^DgpT5_.9Mw#et㠄TV@Ò[f<6?|y^|lAq1+p@W/̝̗=QBHފWed.&Cd*} .MB$ќkƓ B6"A@5#mh<U׊w-AiIЁr@hF㣙S5|EluCuŭiT_Kyj!;,è&v1pt}Jreq,yYV2IYׄ$u@QDV(o3Nr{7w538RM|Ԉ8 2P'_z>f!b2b'"@x&! 5 `E|c\Z\'oZ9 p>Fb agL 1F,m#.mguaڻLa-Β){TkNuRΟ_;wӽwնi0JSYI#;򞚧&l ?ټv955M$W*-?3kѪjipN*5i@SU}t_A%i: ֿq k& (('osXEP[ɬQyi^{{w#9Wd'MʰPRoZY˛Xf2F8gN_o}(c\t+:,nET_2z)= 6UBR<9^"q~nR)4VӌqvAda+]^&>f%pR5ASOggSg( 3q)EKJIKFKHJ*&HHGHޞx7q@',Jhcra lP>!!jN3/̴ʜw'A2FlU)GYQ~%u[鴃Yh/D#zN+LҪ5ޛ@˓5QG)s΢c'`_fS(ږXAݧ>l!Jb:W:DY&OOf0?`!^|Z D`Pw_؀ŢNdS1S]e\g_Hdˀ~f؍VByIx_>Q*twE`g}\UGMEtfY"px߻?'-WhI!ӝ_f<(q5$C֓OOm+I\NXo?8އ [Ty@co;jM@@~xD VL>57Ɍ GFy[&5-hkr̚dz0Qgr,Robҍ$Qz(n`|KS%5,>2qzG4R^4KO}\ն!v$;՗̫ͦɦk"n b"'f=c. 8lm ,El%@myIGܲGn1jst޷qBWYf wbu Sm LŤ0RYe$1Aj'71dtyQ[4WY՜K{撆#?LwJ|=r0j *~)<#'vF!&`ӑ{J}xns>;wm/GT׆ D sϔ(پr}7%#.2Td0 yW,쮑&_OyOԌ2N S SJ<,ZĖ&M]ӊ9ˀhpvCŝb&gkxo;'b< o JQ`+<h8'<\R)d,_9E-s(g{;q/Zo}E.[21<6 rl&p z yuX(ofXs'<&B[xNU0YAI2z7?.PkXiZhޓ80}ѶTnºEwi5/g˂1fWPTFL[~ Ş't"ɓtkj52.VʛKrmbsy׃?t=ŕHrsm73? =t{|_5kwYYΣq8Z(ŧ.pzmX·X 7r $Fٍ3y\$dp N܌Eɨ{h+$ I:mձa$-qV^_MN Ķ{s=M\+32y;tkip6$!} ݜǦ_MzN 6W{5fe+d2wm|*/VK>a:79hl\4 : ᦶ$YކcÊ ĩttv1@ w7;6Svzml̦lJ $Mn͵ָmw"lL,,3tUɢ" F敄k;ی&fp,.ziw9'~MZƻ)k-3.*tZqKRk&kQ&Cׄ9[Xqt,:Tq.'bAm;3+Wx o/ ʢLT)DOOyYR.1lBEwf_GAsn[p10x7S/jJbʉ4Y^lMI- $! If kw~MzFjbN!k[uԾCFl g[x !OfDZ^|Yǹ O Hɢ{_k]U02PҦlX p--ܨ8=+b1Ju=wYe5. yb Yޮ~ im;(AjM2f^=]3VPgHt,8*=IԚL&'2d223u\:ׯgΣIc9  jvլ9-b.XzeS:BȰT?T]Ue$̹ \۰娚dEK ;lla|rSkRU?קZCе#y)~ʴƵHVD眧#8qHz^$aJT FL;ŝmȯZK"T((%c3H)-I5E%3(}X hVy1 LjzH[W\ lvl@:8A('hE~FmՈi79YfAN6ܡ6n1 \9FCEjjn~lRvª^[k-2y$Q{9DWi7[/A.wa8SC <գ}4r1˚@fv׵ swuGƗ|0 AcΟ>2"ޑE|^!њZei*7HOe*Orr4l:RV}YZxގxC 4I=R6WOUcecަܮ=H4&313K┇|wX]5)},"3W>\$7eKrt@{b~8z|6uV 35NxX#P,64h[6},I2T3̋n}3bą8y)v}8ZHZn__3~2W.Y'BX0<>]{3hA"If?/{ͭ g1?on- +OÜ{ 8S34l*fl_ R Y=wUt^6t:ބR9:)}9`'Jk=xāsk+.BGI\A˱+b?eZ,iprW:t|\ұ R@說H$Oq ||;-]ijj׉F6 8gɆ8%\4+'9ɺC1n 8Uou#YSa#k_hrxF}]$P{&%|-6m #:vp h[Ca+|{{[ő9b-{R vs&94#n  ^&spZzsK*R1ckn.S{^=8gjxQlK3nEN-[kt&f=4=&1rsnL)rnmr'y G"xi[^Xi1),b?iMecbz,_`K~>X lzާ&Ϡ1|$AyRbe﬑ rŦ|ʫ"^-HhpZcEwf8[aSqr45A\*+ox#v kō)wR\mɻέeңCh)zŜr0iJbHO;|^k0y [!I)Y敿5!y|IkХ5d5s]nXp4lLd߁ή5E9#K}B'wHZX|HR+E#=hY^CzO>Ѣ!S}߉nR~nC._ZG3{\Ԫ< ~|\G V:Q2L r9&TJ&HPO][Z8ul"OC..-E2A0rjFOvJu3]J;fUo@IaR+(PS:i!3w\B_pp,JJ7s9B=^pS20>)*5NE n&`.u AZ@$Ǎt890gS(xRFJgIv% l_W L;$ȼ'TUsͺ>!y^`mR0nvy5Z@,u&+GTJST"Q3,mpcjpTC>ԕXEiκҢ:?mJNަuC4HCfiDJϊ")W|>#]c_(2^ w58k-Ov? S2ќT%Q93mʡ5%LHeYЮME6km2z,*yEP9N7_nc%.67ԛ05}iXqYR @>#Rx`F(b Yϕ +<&x|&!i"D Vo#SaTM.0RޚKu:"CϕY k2_3?32_fV^;kS:}G5Xٜq_krrd"/7bFFkK C{voD>:5[sJ@iRXhz/Kܙ}+Ii4Qt{U,t"iv~R=r9Nn=̗) X#Ww'f&ir)GCԚ8i5+szUܚe55C c_>|$AK$psLJu|4 ix~ecBВ )P QyBjH:$@z 7Aeu)!%޷T!LQﯕ2O:\貖nUtr_F,/ބ0bÆ/Y,bzÌ\ GSf~|#?:Pwޝ\-`fCR@, +}fr:Xr/:AR9\cBloηURY_/EQAСU-5j] ?}I5>m&Q?WEV8/r[EbJ*gGB%M.ORю~0.-J>0@M I}/a^n:l)x"iiȎ(WTI,K׮ce Ld¸qȞm){VQ= nSˉ/ɠhBVg%hcQъTcJq .vg}qiرTZ,m38ȼסve" tOggSug(#G@Cex$Hz&"`U&z};>5*+6IlS?Ruv=zȮ(qGiNzBP#VM[6Hc`/7:r’ȆBnf4E y73K8gLpJ4~n-T{=*)*†k"1RjVc?>hr[> io@Q!%>E:8VZ _gm݌9t뿛˖޴HJ$|3Е'CEo*@wm_Wl%)X*[?~MWT54YeL>jfg&wOu;7.7΍k%abʃVFK֦%7VWfua*xH ~|h8'o{J*~YXw kܡ) Kmw.f{L%Y^ǎsc?bix1}&# +Jv a.b62V5L/vt qV IX]A:]~OBx]>D*˯JbZc:F\A$uSNn[ǂ~q$@O]$@k{YA)PffX☫X#W/3&C{6/>/i9:4M:^.!v,؞!*4iZ>lT>gӂ(94B& _FъwxX!tG;e1dAf֜rg4u\oUu'ovp̅/@<|KA8\cz GIS\ܿT٣;6~x=}}"eLI _)lS2Кbv$.K+:ILp; EZ=m'EGd w:a]XK3*-̴#t܈lNmcxxWG8.w,@VTƞmL3*ρb 1!\C>d%"SnF}nYݦ:\[COC#ZښwP)}ʖdžpy1u^>x[Or1Pn5>ܮ=8@Y I^5 =ؒ$/c٢r&/D8kn\}"W!~|vR 9"D ٻ}Q6EƄ|6`en|:ܩ<9nRehi8y X}9Ȭm3ZDk:p\\YdIn\cw ]a)UyB#:%DxŰB.< R1SoUv 6"y$wyQfqب>VhAx1`׾M{РZdؑv/RDf]q{0q縆xu% pDt[c]EDO3tk_rk qlSQjj~ekk$6L%6$C oqDz}t( ^<0K*IfL䅧r`஺N,v #9il<!jh_Ъ"<+X/"NRՎ}k5 uKh5yvkr|bbyg3WDeS"u>JJo?yVfLhr%r6a: ±vuw G˽Ekiho-L |l[)n`h@C1{!\~SL3KXD,kp`'?,N!K10q %y$ioJ 3Gt-]՟,nRC2.=z,lY?IX*3M91cGӽcct]hInb` W&uWmU%VF IC4EoVj-JufWjt01a8!j27MGVXs ;[*{s,pHi)Iz&M^Jɉr.r4R-r. $>1 ^g-? a$'@hϧ׌p6s-uU'7+I[Q6E74j^{LxUZߩqĎacM'!:nD[q3GYN8* עvbAL{5^y_}X6'D"yXR3CvUgֶC,XNJc85ve uV]~W >m{IA,)7WstPxn  >V&C,vqN7[].5/\k*u=x*# &ZdU:Yy7otyr/\jko^XA-]JOm3ዕr;Wͭ;7Z{@rI CcY7C;^6bfͻDB[3bx~| mC#$ 6??7?ؑ(Uy3kB 5& m}+0ejҨ;ç]:jLvyo唝B_9Mc*Xӵ)^p~ԔJ@WgQH_M^+ZWIO51O;ơ$Jn7s㸸-k׾-DOG)j1|dEhB/@27Ȏ)  ~|no+NZD4Y`/jIhY' X4V ?YUI!Ae)HziQ-ԧ.dDn| JYl/xf8A❂mM˔8bz mZf)SHQE`DAHg+=5%52 <jrRQn U{iH;^*5g|%5 qA}w]C&zVE&rt듲R ʼRtv@؀%6:?nMzx Vgݾo1o4F5mc.wo.i'P]~ 74S} iE敃ϯa}dej!`"fY0F N zA<(mf1(K+L@>y˂k zIBZ:T -pGl5EMm=;:[`l0'#԰~?1 ;gB?w> (@b["X9Y 9{>6^| D @"搤aM_ iSa) ** ̽w{R)>eLHMhLk!zi7ߵJD^66Y !sBMݵh0q2~IнаĶ;7o I5\e7 {^=eYa xm{:bʛF;:OggS@g(H'LJIJHLHF(+')IFI'FGE,*,IH*JJٞm+R$="’s1I ~Rw("dy).{~i,S7j>Ǎ>=-dh!WF^ ވՂ6T(*J-X@8z9]W\^ds8mifJI˶9X}sIü >jN7A 9 h>r\%{ij0L"<ߓJDRrs޴@CNӳ/E\P*V2zdh7z0#ѵ+O_ml}pĠ{M\;C;n9?6"/A8Y 4Δ•iW`D2kLZpwe>;PF?R`uȁ^eUwi܃s0.Y td׹R,I>5x-NC-ZCo12zp.{;=t0(}ŦEېAA.jwf- :6KX3ϗЏ@ ~8ߊUw1ْ?ogׯW(5ix9n>x7445M8O5JZCqcuMɅ]kGݦeܭ}^~}/!o\'Yn=:j &:S|LνyZ1޾Ӷmqn_ƥpjP < %u+J7Uf{ۯUDgP:ȨS`osdãNO}US8UW:VAu-aL*'0ֿE!#p oMyΎzwﴷ^'3;_%1>}%-Jb,)V#}vn|y oTtÖ*^)#?U{BJ1-_X}#7}O.5[)'[D0.: 3z.Ů`d3#"ޠ}j]v١#Ȕ56y[EY9-(b Ò}9Zb ck?y'?Tй+,:IP7{EsEd5jfӼO އh _ {-zZVo#9|HP ,&ud)YݽVT]Laa5Hr 4Vm7!g-שJy)g>6 {ݜ ,OMnI_wg uQ$:a]1\40"rQI_BS:CT>]_p߮ރ~}/b083w=EݙL}EymɆ/[ö~m1٬/k:uB-}K "tw<[@T[3D͙1䟯L]pQ(T4cؐ OF8bqnn7((X~LVh y=f;o:5[lpL?Y-볜 e%ғC4k`rWW~TmC$d\Xmx-qdFb,jS@H8K Pĭ_'$| )D`&bTU.֮f죽9|X-%AE~t})xo5~J×PCs-;|;uwOKձ-XI]<3{-pΚO"4ݥv]uy/=;.K͢aw8kGrڷdˣɂT Ίy֛@rBX ] Ms nnS5BWH~6U`m'?3k++,:L[pRŢ}}v&Pݗ2= %_dEhRKhpw (͝p=ΦL0 +ܒ6EC^Vv盠l:}{kOMlzh nILiR Iyj$ۗ>\~Q)(=7*U< nOE9~3kJaM:(1Bmdz3ȝ+X(aWP+8THֱ B#wvmC/ej rQYDqQQ粬/FX'ljl$\ ӏ:8 D+aVFT Knu 4LPr g!uygq l%晹'#psad嫉ٻݕtWCsӞ{Oݯ_dx=^U(Ht)OڳBۏ;~kO#tU(τNlؾԟ?ϻŧ>e9T-#3~B qQdYK2@@`滒dg4@FMkQ_ߺÔuki ;Ugv#ou D i}CjUy#>ټ1g&;.M kk`sx,|>V9aqLz"\bF+5F{=rX -<$DK<;_h[xQ+x9$M)2wO>Rd< V'+`(#03T' ߶̇9u: tbIxnJi?{a%r( FA?7LOEi}%}O!*@ŧn WFgEhzk誊Iہ@<>xԣ+4J{<=۟i7+Ky||5@4:MA"i*X"-0$2?Bʊ&sKtko"Es֛lEnd64{iMj R+!^_QAaKEsh3qhtRP#!?̻jVyIv$=ZR jATct+̟#sv63=aJ;F՟ذhteQph̆:L|Jr 2q @k?\Od)/.~PA}P7x=D@ `Я ճ⦧_>ANBF(&{K<d9ݶVV5ZXP8  M;W wf~JdPe 2Ynv'0#nRWOGTFoj+~q |4U4#W&^ė |dl :q(k_} YR<,'ՈzKڽھ5_n>$8cmz,YlxVAOi.qJK?>w ;tU{?ѠʣlxOMTߙe,`1 1X  ~14ƒH;[d2'm [X޺|zi $MJاlI\9)9/yoN)G|DmDi:cyӡɾNO%ul|6L8.٤YRw{1/Գ*HmδM̛|wstKνCn**ߢwƭ ]:82urU C\2Q>T8e ࿓a_*,v>:(8@7ahZsS BiYy{/G ,rڊ,LpSD&0ίY?/ĺζ}(^zG9Yp%:tji/T b tkm:f@ڌ#JTVɸ"pzɽYkaX7"Z(3 JN={Q'Ǝ<OggS@g(Y<{׾|?Z АAЍܕ,I i/pL5%{[n cVӓG=y*`0hqvYds;d#R)6>Oz?_R|(Bk=½%g:J$KĆC4/1ygwnr|y-˫R䷶wYe'x}׷ >"Ջ=\ [V~g|< 9MD fk1(P=;c>ߞ9tXV71<$ZCEуX}+m5S;ָ0EZn?n[uYzrrQ:^M0FrMXzi5:Gzaw6MXr]8U뚤/@7d֭I.^RJ ?ͫNꫡvEL6g:knz{5$_?ѼKv20͙XSG`$F8~1D6Q`FVV抰pw DVykuϙ])fɮwEWF,mY*w" rF-^_G$6$" vmɤAl3|?)W]ջSr\6F9I3UjЋh! [T?فNb:8űk2ϞN+L8l[2{B^:$lÖ!O\+Wp4􁬪ݗE0\d)*VBy=ā9hʰlVN8V&2wnH%~|Mb "/\Dޘ?}1j:lUjX+\UD_L؈OQ{E)S S_L6+S"LJס2$pE +'uM# `0ԒH m OTАnL[{)k|3̀ _+u:jv-mچa@ Azl{&2y7$3zu4"BcfE4j{ڎύho{MpIAh\;BD4]U l|@[թW$A/q'YF| zFAg?IX_DT&kciuNNy-Ύ!r~ ^ga!Z~5T݂S[=o %9ަ ? $$Y1>tH:%ɃmUUSn_R6?\)DPiBMTS"N wZw_ׂmWV*p7NБhܡҸ;;1"da{9/+ w#3n~ZJ;)SwZ9bXSvj+׃%<=Wa]wyOC7|,koL%<:jq|} RO:!pH mQ5_47:?>?W=* f7xyhѦa kŷsXDZ,BAy6:m vOʛdeb'_Rr%>% rdjBKRd@-U[l 9@gPDa4||ĭ2Etl\e?*`թW*%|{$쀐H zC]l0 KF Am9- aY3nLbO%t\Dƫ9en1:>uU(V u$K?CP[<4u7}񹥖&gok_MI mY'iөNrӁ%mbZH( k]5`5,Ss;;/%~|>{ blh3&Ԯ$ ẎwG ޜך%(\u誤[teh<|W;sh{t|z>:p4AַeΏ)L?\ kԟL'kݘjt31Q "3 {р ry%.lP'}N?9W(>_[ N, ! ,mP%t!T6sl8,E"5Ϙje;V4t{^914P.zoH5z{E3H&8R|x0ұb3tQxBW4y}NWY~5?OprbO V H`;P6%}(*aW ")e4> H^z(D$Vu-,Q"Kj 2Sr]jK "s+6Fי28o^7D`שf0ުyWGIrXym8! 3rގ"!o 0=?B.?BGߡj,%tQ?u}g7ŴK3Gr@^| 6Rזum-wdԙLp O^'üXFXts~ge؈hbmzmx gh(1'Hb ݒn*S{ Рnd9ƱUh5~S *t/SpMڞLs4|m_S[5RR=X>| 'IҢR2w2GCCž=.9H99p0Ai{aQxS쒥GILT Z94.f3F: ~FEf#aM ںa"E3j~uEN.seU&]h+Ǝ.6+&=z>PHԬ4, y |*vgۑ6A@Z$S䪾:7c<eMKv|$t.$N1a#~hPby P3jkU@ai7#h] aju4&8VfgsSI<5I #ҭ  wi m/$yT879N{f VPb,|~\qx{zdfRోP۠^׼Z9M q3;y$j2'BTeKAL hLDV[>FȚI/~VRrRv ,ߖpT(b̭M泱r0Tb8}EZ 4Uٷgk%\lTVqc𪾳N|sDO3F w¥T9xiထHA޾ vH@뇦|kqٶIDT* hjF:q:t }^uYɝX័cn~1My9Zb@UnT:9λ5":Ɏ|R8 >UIL|rbLFJ35Hgf xbnQZKjX/qkD3:$}g B}695r,|~)YhC[%5QkMf""eIR1~eݥ?p:(6Vo F ˽Z)IׅpGq l ^Dz>#+ke*P&Ϟ͵sR. zwT 7xXAIpd]ՌeX#B l!{&GOggSBg(M!('')JKJFJG)))'))EHF|޿#P=pE)boKgA. wp(S!RU " &"sH}U]d"ۖs%ZCDWj|n tfQ)6]2'V̠wW58!ڲx0֐~.K|lk,e`@w{'59:\.R$EXwOr {z2is0ll{hq9^d˳̉Yy2 Z2NQ3TV?SβG\N_E%Oc=֧J=BM_˖K{b~XƲ#Z׶Ĝz`/ +^|jɡ'l{cD"DhVGf0+Vd>6܆{ͽau<72ͅ#-%J_p?wF:ZwVT}LelH;_LC^a!>ӺzIFC7|*aoGPAِD*q} B#,KYd)QQ̞ba _qw1FlMW2V ^]G$& ua0-把wWhzd"Ųk/ϷFVf]}UF +_2W'<>u9MR2ϸn^X [#lDg=å P25ioR44oYo;2?4m $\7io`}%Kkx) L2npsO3VujbDOV?Ea%(nTؑT@Xx]9 F2VU9MMZ\4zw}ख+WO( jjdK3yPHh?lb '2B,Z. 'YgVFCq/xXM2^Z0Ѐ  W y5IHWSg|'s+P6XD$hr02)"Ҫ\~ Ob}YM5vBY99(926C)|a;MGm\RT)z/SPq";5޷N0T6!21 ,$IVbGݪ{y{bnaL&|Ey- &dgK Wm#L\J2Q:2k4lhK/7}j[>3r{3ȇ/FZW (s/ V&?+;Y*UŶŋ6^ʼ3{f4+ SդN(#|p-ͪMl"~C5\ձ%{@5VѪ,)@ 9z`B_)f))_7kC38c+q36Vuܦ;TYi}iyzmӊ {QFT9y-^Y~~zN_KR4Q=6mtW k'!']A:$0ocs܂M֕Ċ HX-T/pF-N[iLpckeRή9y,I>|n z沐G!Y@F(gTݴ^?޾)7}0[,c7ݧh.!A/}p5bPK̮ݥf-Q=B4g9I"4}M:%k`%Up^ҜrM}a#& (ۮgIyp$wە^n ҿg>;ɹ t\pYp>|^J 0@w(RJ-u"_."Z#3 = :|\-۟~E29X/=f +j*nrC(8 9sznR}P$[L" ߩꔉ˭`)jǬN\%GFTS\#۾n{tXqCHv XEo^ߊ'Dw<_SR@bzG 0x`Bj5;%}ǐ|l/>+W1vص,>M,F0X THG9$ܺO0-Om&xxqR.C{-C@⻁8jH& d@ / Ϋşe/7hWtlbC)-%HGI7\r]^F0G +Z% ۢ(|RcV=` ?+˟O1hNx~LBj!8SbvW̋,B_}"m͢7IqTT24txTGG !FOc[-%Xo? pC#\f4[qpRYZW&zq1; v XgD!.9U]HqahL^Û֦<20n#l c5Mv ʯf۔{߹S}^gQt&ެȖ't͢QcUD%IE&֩@F5jFE<5Mr(94[a9㳯(nޕ݅3roS~%ʌi=>}6,0T*q|lݛGSk6[ww% / 8 3j{+ٵs 1 G!vuVSq}looտA_{#6! %2[@[KHBV _x>??}VTv~8~v}hb'|7_2}@ oJYY5D0|`MWr)?:݅WojGur5?( 5Ls(72{N!kn-M''ioDj@ݍ-WW;3CV`UХ wL. ̑dkp] x,2:ח> %I{(}iG*-PZϺ} &O@ #n`  08}'!_W5N(fV;NDSvVN9+,_;gBW!%N:*ɽ z3?-_'l8㔶o=%.x;g`=\cļBjB~ƹfūP giI׻ڽ32cWs&ku~ۗ )uQj$]odt*zSmP`4rڏ}J==y-vȗ&OggSg(R3JJHG(+FI)iɔlWߝ[! bj.[ܫa];ݲݜx?~7K]!a"UHZ3Tbm%]tK8]6'qnt>bQ> I@OKO߷Tww%hG3^oFp|뼔xkGuP[|\רm,tJoDK ! xZFh]]T *}S?ϧ_t4c \~8V/txy.ۂP]d~8V!lgO1 s%гNU`iEfhRw.:p EJ\I 0} |ZkYYY(lמ8((r`Rhj|o63.רF:3>НM9\ס;>-PR xr&&K؎S)Җ>F^.VyP頻\FQE=qC5t K+Ym)޸l=vd,1fH3AK x -x`ݏeG-2{=\&hj9">p*3oN&[w%ȀSߞП \BxnCONAtR>J >)0ٚ=U)@&uMzFW9@! /dl`) b²S>5bl|Qj%dOu.vvl1j8ֶ<C\yuw '3%{ ӻ}zc&.-Xk1&]-zf}T8ڌP;`<GLćFW\H"q-{|.H ,VRʴyI-zCúd޽ʺ i`f`bA~Ý2.6Um %w U$Sd1o @y0j@yļ1&#$g&4*UC-bYMH^`\.ӥV)?ƛl"Y935I/y,F_֭҆L[繝ˉƪe*Z/N7Τ%'kAr[E2hͮѣrrokGNȤL|݌$7v[ :Y.(3'QfH7]f]A.l=};׾l_>i]Zz]U뵠L3/u+ἅ;##^@}w/:BrK _n_$3'z(6$DW5/+Ն8U#ke'^QOiJYH*@Qƪ0]h+GMؗ|=zi"PNг:<eq|fP6xZaۇOG2 :7 xꍜdhe#_a c=OO^Np!oPTEq;AU [/ݝh&7+RszDŽQӍ&\Go7-֡j(sӺr{ĞCHWƨ7֏]-60H~ԸK?M+~^f1xe1͐Hz5ESot fk$Jkh FuG#V-jWиAc$|9}1( un, _pJO;fXhNGl =0rZ qo-!t?8.m a0puEw#6/% < ~vK ?],Bu]XB׫ &DKli~I530: 릪|E'~A/9NpL~T G݅ L ̈́哤 *Fzr"qOExNvs|=®y (JVz:u Rp&/UoPF4̢gbK9|][!p}XGkM^$ |wμ|}>U0埣ŝ-I{7Ls0%'PHŸX\{1~i:AdJZ3{R79ΦĘ]`_k;XДȤڜI3c#>_2Z5uu{]b[)$n]L_3VŶi0= zk΀G-(|n f3"$&`~i,2y>aj?iz #85Ac1cP,GPYS:Ph-iN˽JWXڏ[v eH("È:P+>jٚ87i7Wڦ=.]aJ1 7Oz-\F!ES M?k 0;-eM8dVGE2Qc!t"ƅRghC]Z6?Yj[e'IⲼk!z:TEI;eLu?2(?6b˕ThЈ֪o!e[ss;jcH]C=O pLPlS[*ݧW5e<}y`ĭe9;G%0>lpxWZ~BJR(#@ HH#xx:1gQ $If42YvfTeŝKaym VhBPzԺM<y?: 'w Q'n_Ӱx0kO`yh<<.3'B@cǚ5 ~|LE b t:I",dx}7+>_*! p DF2g &%y/&[oe 3 r8W8}mn1io]5VΖY僜 Kf1iiH5n@*8I&Ts ޻-}TMeMd$q-H`[dٚs:'qjf.I j"ZNDF5HM _úK,2ˆh v'98rjդISZs?6-~ʢU\6%pҢ5`)5[s IoKocV}"^-җ&/M2%^^v5S0㬆!h -#wk!Ю;T~' Ӹڐr >| g`[\f'"Sdj'ሼ_7ac6>{FAj8&3AT] ߭#f݁4W傳 ݶI%Аs#}qH{*SX՟p92FQdJ )W<Zb] u1C{*1?gYa2wOggSg(N~/޶5i4L8DD^IIf)p`R,-ِ*TެɍELD(DOť[1Nv>Zu+@qH3 Θ瑂{nV+7D6lq8rAf}?E _derъmb 2 YvW9͕=y{ȭ$n w@z^|mKR&1 uS3K1oEvPߙhtROc#_eeTu' %4Gs?n&YޓZ=Vq 3@LN>Ȯ%I0klwCn_Uo[sc%CHp$sXEYKz&(F*w!K%|1xX[&)2MY`-y} xg`YDX*@ B4ƻ tHKuA5RN&ao=]nMg"idk@w9̚Ft]izI.~;Kr8$Y\볅 }rd kQei](Y}Gsy)+@VƏx K HnDxY`䳥[kN`}/fr`@k9q2r=uˌB4LMDll²nXzM@YxPi%7r y% 8A݈kO49)bkQo;ڶBz%&M\yi sX=p3k$OZH,*VeL|^K x/e C23~Na)D9XRK%10s OEow*]PN6u}"Pa%\ 8$\ rڑ& n =-2D %*ѻ#pR*>ų70Ɨ_i˜&aec? а镵o2(&ѐ% X^)ߠe]j$d'z"qm)4gvKHj.M_(B5|yf6' ʉFoïw!4_"^NՌCh1kqѡG Ɗhp:IۘO "΍zb]z_p7`VЧfdR oƃv0(i,Cë4xUt$^g|Z;M $sX8 Ng&R7N -ݮU$zP)P?bEo]Ҩy]}$ 9rSnZը} 蠦^˿Piǒ2ZأH>FyrLo8CKmyS޹]Υ.g+甤ypB`)%7A=סF<}ҨaSQ֒#. &d}w%|^0&Q5a2Y `m9W|4^iɾ#8Kw:^7"68ZL43&ԮzNXK@ ,%S xS &>xprNEcDw]P)td7}8׆?%5\k&+*EΉ͙zf r&+L?UƂZ|zCq׶FYk -ܐIso-=ؿS졓th&;`eyAg}L|q5-D8g8'>t$\rդO;fs:wiglbɝIP%L=!0[n UaJU}W^y/=*1*`>Cv!{zw}\#IB6h#:ڲ fB=[zv|=4ͻH1O_(nhghdeP5x>)bfWrldXڢwz08x]𭕺qK-,p QXJ>/EP~|lSH"*xRdHZ;TuB+d"X@7܈ 3}'[Niד''=ZCll?M|ƶnL$i?Y=*Jm6|ˆrxa&HwfQo9fYK #_.}sst`ƣGߧqb._Y,nKJId˅yL V!8YT:g.֔uUȕi@+-U|i"цdy=/W~0_Tsn`Z +O Vl6~$;^%wm4Pd` dy6oMЙLLV4RHg;_SE(󲜈T5W#\Cʚ'$ I~%[⩴ :Jn(a^$; ]^jɤݾyg L,P֮E9֞go~xI{'K6~`(Yy >v~܌<@0I^b!Y8sx +9d}ݧםұ+;޾nř*=6=&i;.Ib-)Z jqB)cg>fygn6hgOEP'yo5 )mi#!_̤;gy4ME4yr?D< '{Ev/6>a1 %OggSg(2LGJHIJKLFHFӞ|ZI&u( pt'yM b4…pП Y1D(V_Zs P3"oh_6taq*5]c, 8rqЌ0$#>R\]YUE Oh DpnZ})wԤv 1 F!Y%P:5:l _ fNÙ#ԍ/pY) 烟^|9^~Gb'wN[:~,>`M9UE@/GIato*aYP*hrUz-i#0llSN'=uetX^"vpo.E͏S6ﴓ^kx!G }I8TFvF8^|JI=`@Q0u@]$[&MpVX’LJgpyۛOs,By%nVT"EԮn@5F-fYpݚL⁣bzy15<'Lnt{_<h8(Qi RIg0!2-O+ ,X)jj.X2k[+nJa}8}Dz@h6K\q!?dg)Hrf@Yal0-Sֵ3[mGh:L돮|ao2ݩVpgbevS'~0S%?]l S *HZ ߽(>!M \Eަ"2{[QAkFks~]i + +oZ?_œYܪ.^OD)Ņ+#oGBOPCSZĩY75,mOWt#8Ղ}Ysjy`̈^p=  Hze␶n&Os Xna\,sd7=v;1G6=YmE#9my9[!>(5C檣ra  ̥l̄Sy?MryΤz| ZVwb$\0߮\2tjsyz#wF|M>ւ% %qޙ/~Ҙ= hĸn}rJw)9 %sK{nyzmn<<DF"ƪE7N6jZ*j0l8xslݺQ#^.y趾3L4R -B]m44+ZO8:wmvۏwηwΒ(K JQDZySR~| ˸nYh} ӱSe>J_NgL6Kg)mcMZ1@ӘIpssϘYA X8*H`fh:}U}~<ۻͤ(T]Gszk|9ʀd.^ChZڦˑد ̿K(%r&=u&yB޼Yk()))kkMkLW eg]dU_v57 K;@/2.׹o 5Vg(0=հMI .TRC [XLa{XnO{ B$,l06}1JO@#a5~H$Tt>Hܮ@GGm^VRFt-_S pccx!%kǹNS'xN XM|\bs&2Yɞ|>3 ªƣF]y|lINkn / ޷,刵~wS&5@&^ 8`E*/Zg"^SG+D.Zm6zѬ[ϋb^\1%Xv-lv\|Yk֯6Iy $<~Y<#d7g%p^;;D-SNmm3r%E',y"8F0+ e"ܤR ^`\2GޫnxEhOj9kS=B_L긇UýCG%fX< }\koż#`9Q<$ՍSmruPyCo"$eX,kOv#I6Q[~SYf;#x!O+Pw<9nqy5ƠZ) 6k G?K~s c8 ,K6aGz0\#- '$"d2d& `jJ$-h}qQOU@I B7PHb!<>@pLNoT^30",*z׫/H=z #kM[xfnOMˁF<ͲUN,^Sϻq߉*%&2xl2HHDX.޶|L% Ou ,@wW)fI 88H)3{R K C.-(Ʉsx4kOaaXbތ#N<]$IDePy,m sKu2̾5 eQΙ1+$'(Rwd d=RMN*B*ZTX'F ~ܮR#jxL$8 RԱDZev)|HI܏ -IĚHe .1R =^Mն KM; t]'Y=@3k[$_}Y轥«̆ľ9q&gh1lf1`>?kR0E>D*k5@OggSmg(O^ 0 GG8٠j:X%[^ SI2;ɷJCUu {i/0U q)?Q ]iY3`?#އgxN6BEpOw^}$& j)g[vŔ8I =zpo4^1z}DtHW50` tc__qܡ19N}F)5ZK~|:H(@!"G$}_!nF/fӿ O&G^;G¨5ق^uޢىwO8Xdj1þfx^*Wȇb3p ExOTnd~Zs{Xu=[lIm'i =Zrx:;ͬFTzKŐ 9 l#(Ys\َoNd~`w2i7&m˶*+y 5̃YH@v<sI茳X= |Cp jI I'l08 8 hk7//+WgeCW@Ú c}($ 9uWeŁmW5v&%^M]eVĢz '_gdn$饭qzr~;. kB Z?LN 쯫ߜʖMBBVѠLtՐ!0VG@}4J>`i{umi+& B?#?u[q2"f0ɗh^eL赋Rpt.]-mC<3[z,ak+D"r3 :k) niTVMEuvNbλ?Cq/DX Q^"׹EU휑A\&̸qV1r=ۏ)0uyI[o|c|d-_gi¸NK,ymF@޷z&߆^Gd6ׅPÊ;7ja-n`MDMS79ASqtFuH!~񼉖ĥZdUFs#k^Lysovvd>ܙQ>iFtƙF;Go,?|uh7xhn;JFM$D 5#_|G[1~(M#4+#^|?p(3ᙢboJ EίN'z%uʾs5cv2Уoqy0SʂDD%$/5ClFw3Ta‘.Zqa(Б/GAœ'1h۽MW(~ n`j.[[F%7M/C1ُE1*9 ̆>^fd+x%lgTbK&f*WWq.ܹ u7CĤڃ |!I%72/b6R"kck,a+E=T8R@esJ4ڐC~vD%W}ޘ6CT. h_EWeh"uZdz٤‹d߱?ާޟ2lр~hb9PY֝=q.] $:@ XJLƵTZ踬 o_\!L#[8y#_oY" G6J_4"aJ*V)[ }|dٺ#PK7R~4M,H'h64 .x:X>_dUhΜY!L'NJJ ަ#~䁩(54Ǥo;Enw=AnrDBVT.IWC|7m%ʋ?9&OKdxBì-`pw]iGuՌcn7)%}ھ9i:D&HD4(FI*u^uE8f L YsU_%Qua,5#j98|^lǟM3I"m׭<ס$I)/=b&G3]mz{5Ev%oxY`5PR)`cJZjn#$bt|xy}i1K!C(U%o9N`joarOnTUXfEM6ƮI3SƇbX7Fҟ_ J3Jnt]{36@9h<•Mh7MMDU꠾tuoKfPPaM {f2wBcV_[tZӡgJ:CIn5G@aH˚D;ug&%j#}&P8y_JVܧ֌6SPF2dn_ B17 sPb\ RlX^|:fˏ)"$lԴ}d)Мmn ;ű}WS|O2b F )fʑ/2`Tl? ]\^1Ԟ&ݽ>Ug#q/ZZסg[hCe6ȶ#~|jkAOd泂@, $ͫ3lQ!WID(8.N6BC Q66`д"2]_CoN+`iWn3eT3`4g(mLD?EZY_to$H5— T&"_O/IoQV8qop"}.s'z*̹k~XuPޙOth'!(tx IB@#H)Iп:Cnk6&/`sVGi}k¦)? HQ$4zW8&wV'3?@\'(lIY3Tw.j4BoVeOaFYu_ m|S}|AYp=ݠBjTkM 3|8hqA[(u\\0Ǭ𭙿A z/F)uc>U x>Sp\c~;q, f IsJ&_J&&'WyfЊ: Ԙ$@q~u#Gw_5R<~wM'>f2v;~)}S ?u]Q5igLu+IlXxS:#HmEŸp]´vgjfDz_VU n 9WKmUnJiȫei1dPVot@ı9#~ߦ}),4'A]fdbR$vVK.>W5+n| F8>#y_K&jr-\D(:B+`kůܜЗ%/^KVWc;șh׏Qkn {>-Άǂe/8#cP D,Ӈa\'c9)h=q[Pt>| Mxw' IB7"\f[澬K7+ҿE\Wf,zҊ92)UZSL_2d ="> KzlvL5=#JDV9m8a 3[ ~u,ziΙ!LnTNĥ@D^6aJA6%6v "9C58C\k!&3I7\1!)TGOVPB1'LS=rH;j3Qj'7~|K)mHu>i; {^j|_a7!cDTHrZhCB S22.O^j01~½u `}XՙgSeb~\AVS0p(IsEfV f;f^:j\\-TsGVE4vd]Ǿ;svL'MǾ_n?0NIğ4h<#wuYL7Vt"D"æhX- gr8ui}/or~ ;ZdC`U_E460ct26J`}_gs>~ىebkGXو-%KTY@ ap 6PRa)aX_JEnǕb|TYkLtW [L*xk5ȃMw_](i\٤e+&s"D+1'lX\66-9,%{ɶ{F5AutOvfU{g޻{;~yZ[OL!wV ⾼.~p YH Md7{Nz)\؝/33,݋V%CUz䆕}3y73\>߾*))}pteգF%7- 89u^Zň!N*/B絝6 ƒ}fJ͍W?[螕|>jl+[~HO%'!} ?"m =5hPź][(*oޣ'KZ}heyӿv7)}33J1|1\)wtF(ˉLzblxrT"o}=~;Jޝ9!=sr86Sg:Hh#`*ޫ~4H|i/a 7(W<K i&PH_Mx(>tjS4&  D1ejD"i-WF"z^>#g'G<OggS@g(=x)+(*HEF ˝DSUd!cT6TlK^Bd3aGHغ5mo!6TT8yp zÍ#\ dh; 0x wϿ5_H. fgX 4g;&.hԾAS/-[Ҽ9دf^+كw]P2-٨jmomu S#9|_.?*<V @@-qP=ƛ#(ՠX͌>}fV;֞-˝L5l]\FS9{_-}0:#Zѱ6ԺNn[Ul>bSzWGxffSrF"*o:pғ9y̅#;\BحD[w)|!:>{ kvK&{}Ǐ) a-@ҧYLJx G(.~ʞZjHzeCWc.Ӏuu׵^ S S7@{X'yl{O9*/N|BEU9(/ʕ7twd&%Ux^Y\ O$<..%Vn}o8`,myA]T|/cw6>ȓwQi j =ޓY#󺲐5c^QkB~V7p^lAk 3J5 o`o<ؔ# hx_z(T."g|nZKꡲtTFkuq~MM^QR 4E5BQdܖ٬qysE K0 Ic6 *M=7~毧>?S:ws?] MLl7X\ѮO FxwvXwX td_Zj%y"׶EL xis2oHݼ=*L;ITb5#)$6Ci'aZ3c cW%QIGWH+% #fltiœ߆7P/Daf1s'6$i^EZ):$U5L ň}/A${ɪw/A{olMs\BfN:r zj!LJ0uS]_fSY{;8)1uh+)jgg{ ^0r RP.C2\Y3'Hߌn1XgЏ)퐨VLwH+kg$9-!'Jyf=)3Ǜ[ QýnCq{e2ֶ}ޗaDxKH|Ġx s>Nl`I4r'rO2\Tx29"C)ef?f 1LV4>S{Kl~RlhZ1z֩DPBIZFmepQ v_FۿD|l=lTaA-zh17gKi`U㙭k GP?#ؖ =xN\| bZT_ga  BԈ2+ZX5J3DIK[6֭)kp v~Ҹ9$Y `|\Nw5dwՖָ ?+KxDv:gjd/r[o9m#ȎzrTiAgi6E(9 fr[ކDLvz1ۇp|c|R"f>zb3_;2 (O>hhā e5j/N=;0,%VB~0vl{,ٱsnO+cҀdoj"|^*ڿIK+B7kp]<9 L"A94F?nw2LS;Ȭ4].d"[?M1[PjDo?7s;V9WjW5zNJ|Ii p,=^\ ʃ$|_KЄx?i`=[) E ߔH~Czq{aĕ̓vtA7 WJ-„$y/#ɝ2m.@61mJtagkc%o_N67^~9ECjnFBZYQ5#{? Omi6!G[X[OB}%.*j_>7.'F5)3 en0+ @dB{8u{,AUΈ5/x9^b[ia=^wk:E^Y0ۤ܂&(EWN<݀?s ]"pqLk +,b[xgvѩw_Pi dFyut~|F]mezWb F :oOAM?^7NbE`~g!=i4v%oco &pT`D\/0?̑3˵Ǚ7THD X7K}QR׬uC|*!/J;ַLFIwyb5L$ȊA[W3ʳ3r3U8Tk4p""՝OR :baГX}{0@<+1٠[k.oDJ0~Z$p. "ﰝ֤=I3`Q20N2ş`@0[ ^|<[Yyj ݄Dc)^fzN6nG۠WN΀0H2tM&<Ud.6 p+,Vm :'oAzD>|hr5^,S쨤ЉgCT J-l/ F05cȜ#saOK⺞9 Km`8gu LJ007G ́"\dVL4S (H45+0FHLyY-I:836Dg]<(yӱgBA?-'|lUdz&N$pSq+(Qj,h2"nK:$ N*<(Y~|HL\`[)*t:F nr\T8}e`L@)mqn²_8=Gݞyݝ|PV2`E+n}$?RƜWޭL/!́NpXd~$zk8dr`/;0 vj]%,5yb #N! L*z4e($H(A\8E7 YJlMtpIRk034>zM]K?P۽n};m1-6{^x|(qOh$:j]M~^;ٻmbX~|j{A4"l}rRL2LBHiTx#^cz!Y r7l0tL3%pY rG1Za ^`gKCs >0 @Jtf0*~0Jw=L5߻ IHr†iC^qj}~Jq u>6-Z@""\ @)N.ȴ8fÂZZe(iEuI0V՛YmZ]H㽡|E-?@շXJ]\*iWا5]|`C1vM`&U)[icwKʰBm w!.+tjE9OpD~ٲo@he 7֍>DDsF^&~`ާxu7&e0ǶJt Bv۩jr='2|*I"X_cdv\BkQ"  sQ.,2褷]AX^IR:'Cd5V0jx S)(:*}m;WGl@e?\wC,jg;½(0'hgMkX޶<.4"0pýgdVL{J g^u%TY eL+~՞ؠ2gdYQ>Z^Vf-apNZTFyD[,z5NTT߯Z  HW7? 4L׈꒶N+PO1@襠us޷ TxDЄ1杷xk\coANxB >_kCL8MG;d67 [qV*ݭ4+{)Cj$Зj)!Lɴr6*3m_"ch9z݋FL*TT>mj|9P_깷4` 3^Xj"F.ɩ:;Eh6ɏF+[Gѿ!^{dCv;exc 8Z0ϲx^|;48@ P'pHfPwi@=?d)0:iοﳲsdi{.q(0J;Cy|I>au\aRH׷/ZjJ-tAט8Fc EnJP_w]FUX9-fϻE$Ѥo27TgB~@5¯[ؗ`qWX7h cͯi` {J%tSqSkz˷ֳxkӜ]&卆ڜ I5&+6yFuDN33=CAZky(,J&on)!h<1 hLI_4t-Z4 |Pw 3V#q]~շ1ft9]Y-)hDMHv9fBQR nTksuX'xYHI#@/Ыx[#y2q4?<>JL/Pa[/0~|_ f~dMO wI4K$[3+_]t@Bf,/^jǵ сlgg #6@z7ZyCA3X3INeX+&t3qez8Wg+SLlf~q$ȄvEpR Rfyh;:JK'qwIqO8JmOߘ{Dq f| hw4fem98w|=X p|$đeHf3-Vhvtͦ@k=/eG!-ۥ_H<6`3kC܆?-bufgx yOnGѭ ?HdnI>ZQ WG6?Yf ?`u+l/e}"D7B; z T\ol)=x*; ۂOggS@g(c"*'-*()+MGIII*()HKJDD޷V`8P@m;sQ~#!UR|f }1&4mS=W{e_x?Yx̩|&i:[9'R*q\)Wfy&S, jShiH10Z~k atm d2ފ#tJaE0ո_y a:(V'΂e9j>;aLߊp-G4'ַmU~F ulwb^|?4hxvD8X\GC 9ea҃ERgF)QX1oq=%!|}*g& eڻVP1d4;5SP|J*ZoĵaJ'FN1Sᘃ>̵/_*n.^ui', YCF' HGP"o|nSIb&``X8~)|Y ?-Rqҳ'?hr<˻t*5DSFE,)>zSw`3Oe֙!YQqw)|R᧩/P]\ޟlWduֶFk^W6Ea{L/$YVTfa\jFcgl4x\UPV5)&,USL6ܗZȀN &:µYd"5g_%[kֻ#ԁt[&N;~EC'{#;E*:BӐ[{V,k6!z[)[TAr8\={=k$ƧE~QzJьK.̝}.h=}$fm0F=hhf: 'x QyQS0NJZ5&CgoP̒ @Jb0qi_iӘ>WRR̄)oȊ~У:e\Rx2-B +e4D` 1:;/s$ qayi u| (D%}t7ݘ`INe}> u*[M>KyFy4`!FU%LNl֐F=J ApiV)>nz Wdd2tm?f8-^%Ԟ>dP3r^fXG{h쇳gZ:}8%QݭOƁl{޴?Z_ExB<󀵑~AUz\:>=opD(oM6U!﫮[<$Iɥfou`ګ=<&PUe{gOz?U xο՝-%X>Y^٠#ͅMh62+<s5|n׽x5fp2fyѾ0h[:v_WԺ{sMŤ7-RH9NCwdcH]K'0~/ m}XU$^cKhřeTR +/fRfyU-dB$d–J7,woUyN77&)mݓ3e|V84ܳe,c๿W s>W5:3p.765aI. 42}v@x=RaS?@ @2`6CY6n`xQkAe Z2龣yE+LKd`)S(UxӾꄝx3Mu$BBΑpG:FqV$v<WX \ p_{egs<ܥʩIG^"ȗ*|"i nPKrp]w;[wKgߖ雕mK$oe?NjtzI:*ٜzxs[pl*P T:ӲcZn>VZo#xqqn>d y DVqS7$}F6# >[@/o*>[wK>0^+wT2U^/ \fbҷPzb}-d{c]_ǥ >פ/3 PfXwznbp{b{齟fuŨ#MIw1jeu^]֠ʊu'{D*A8쏭hD! ^5֙t){y2513A 1k1r|y?. ^veܻy8w%[ZlDivϏfdi!Tr"-kh\+٢cʎ;Tw1LŽU./- 3- %[(b c2js|_V<^3Z 6;| 7TK}w{_,nS`R`Z'KUۖHLC љPb{=KnLx{\@\l(J |W2)ɿi&њu{f? '%ZS,|<#Rӧmu;vho%*ٟ75gLހ:%{Ғf#R{v>ײ.;BHBLSHm%_g[ P}̣犆0~T躻E{;x4}ވ aad;nqihfr[MvXϕԃSkٻOqߣe2{}4ԅ4l&:ϑV$Tg 6MNPJY;>jdD."PN{@IȲ!q)E(IxX_\Kr{.]%Sެ,w>7ԽVW°|֦~<3ր#&@2l˕lRս|O'uGSs/WopU'ݭ*ł-B`F0ht w#yZJ21E$M- "x$bnx&$*%LB] :gQ4zsʙ,~7Q/|b3s RoK?cړS'"z4dOggS@g(35~|Lt~s o/h>eJ6*>C#}#z:73l&ך].lbIɼRH0iRyJ~|O9oQZyS\'sœԢܼ҃}O( \X6RϦ37>ϘK͇`^mvGV(9oͥĢ Fc$BfX(hRL2}u}44TBq^|#; T_z#AuN^|[r0 O6^H銑7 DnnC94ʻq%=ߘܬ;;Z?]s5TIDQINu/e[VV@?IaL<|Ԛ`Ҟ8e5@4W~|]{ uZLj$gWVon<`o'hqjKCQdu2)>^up敋_)7๫ern .fPnb#~gH"p%w}ԛI?z_33oQR]:-pY[lWAQKEEbK3Gp<$38>kH4r:$F7gfx?`-L$uYVc%@>i /)˜5 +.>降cznɰ /6]u}I5D@A-_7{ L P'uxx0:TNZ} qz9w*9dVۚSAօo("8"ےO{?l'q TG!{P) f.%j4d_~麷]50iB[?;JYks[FW*i8,)Im`b?֐cK{Ϝ<™^-\NKKsCނ~|#kCY W1LwŪ3jk 4Fۊmf:}3:BC JJe/7~ߡ?Gv +6{lN Xv7:JOZqNX޳SV{4\7$4[C_NXUes`^Ocm:z̯养̭>Ɖgۄ"jܘU) `ަN%0'vDxJ`^4`Z.d z?vcW4B~;OlXwHg7,aj gĶOwHlSGѿ܊/n ;; %tCNmqTb9y*IEzؕվur3LW $tßׅqԂHHĩO{7{Q~|h*)D[͵XI ??ա^ù:%+l/r=אPq&=zkּDfWW4!^CpndK؆E;d{!WHɝtzGYE2b5U%ZQhO~uA wpn<;9IN48l^%46$tүJ|}$J^|_+g9БؒHz6!I `C u|sz3֝ Vo^iț^Ҵ H#lo؆4nocR̚-ߪ|7d_M9 76;["Լ |tLá ^SƁt*hGck1K~I*q]^"4] r]?;|nK`੿=BW/W,*/~>ך{L6>ʹcWGʞQo[>,vi* L9j%z!$lS\U; v&+Ml\X)׃YSɾ48~>GM~NoDzeE >B!@0X>v }긂cxB @Dy:^M !:+flۥ쐍\oN[AXGAy vϩ~ N&/L_KGZ0bG_F9,tEtРܾ)B<&n4'bYGzIT3P3 ZH{NTJgM絊%=2>5uRˬ,0oگ 8RscRI;[)W)ZT|x&Y>-n ][ogpc#V:;O :ٮ }{6]hd;yb;,<hhiY+8<ޖ kzq莁H |:h& H#t ~mKN*0<޷|_~|6lS7W2 κ註^{GyJ"7+ݮ ­5'Z' %#iK\Yڽ~j-˜}dGvPݫ_{}KŘξDK2kJf4~Q?pI$P{8XO^k-לg]̛]š:1E A~|#X~ޭʶI @*& {k-; xhyu#$%v *ckmEnhoD d(lǦSch\v=>t0ٍ'wVcktD?3rjG'j/‰5'L:Ey.$$D+M.(pw85+N+ZM5:wsبSzЉCPR~|cdPyo'~IN:9L=⾝7½[5T'`:@.H [ AɌ[烒krv3EUZbaνx֋=h!pcmuɬخz wA*1B4QL+mvk!œ1 ~//ݴ帠}c10,- ZxϮ6,^x: tДQDͳ#|hqmOZJI V xM rЩ,dcTi+yUXCSIv_Lt7E>#v ﶍ 1&"2)O ZSVՔg_8i_Z{rhcd^|h25zqџ}طD%J8jI~z ˋ*sJ9#B8>&G Y4Dݶ) \y;bq˶}OggS*g(')GJH7>wCQZLe'}y jbGl Ҿ,`bA} Ԫw<]8m:Sхku~.31na mBILXkF[+fvkQx}2ߋx/5R >#o8L~ ߳ M w{L.Tp.J#UQ`pzܭ'٫2hi8ƇKy9w{ p魋G8SkviEC"~h!Oz3 xw/1%_(J5))G>Q!.ۧrRu/7f.5U~.(XS2z7j7*(Mι/>JJ=Q8 an> =ݿ!`X#Rd̖QU)S;\W Ź>N0ӀZ͋SQ7 2jITzv~kZF)^_@(Dlh2-064բe꫑yqq5̀)\[fqژl +Չ Xޣ,v64>e`+HPm4sZ\ |]Z,xsn-"m`Hz]mz}:1ңyس=-mW[NF5@zk5e[vlTih躵-dBҺցg`D4 bEǣmRA65_֌SxoP$I<,UG@[aVxgkn1(JvsuFu5_ZPE~owhܺ-l8W0O  'Fء^<^G+ `WL\78?\;8ײ kUgIt͌cO50Uy7k&#N[Ἀ֯Ċro+zH8 ;o@#vDDyl}ML]5ڋ \ΞWI)S wS"0A ê X.O+>X4!i> sNApM`$}Qo;c'AH(6{R-H%[܊ ?[ Utg] j ;ĠLr$8]S%"JW6[ڶwZ]H̑Q:~ۖU*;| Yv4 Xʂy>uРY'ϭacҌƓ 9~":|. g}8zOP\,a.w)w; с^=7: ({}`[\4Gv:,!w )sb^g;繀d'"lM!WٳB"uH3y"+YŔV9*SSAPF@߃w։58vܹ>iȣt[&λaʉ1`B^5ω D ~ |oG7 H8Iok@&oG|&Fg|ieCE5݉C9_v1ٖiߏ{Ƒ\VpX20syCjx~l0*ГBzV6a+F_y1ٮҦlX~Y{"Ij `m-ębd!K=SoƱ.Fw9}~|- @~ ['0E "3ۦۭRbZFQ;O$gl Fdu*M#_DQ8Ɉ)f+m^QqvE,FOo0t\o;?sl:xx6e)T0E7N1*P %xc@6&_Pmq`k*V] |[ Z+ ?a'ESr\8x~|5WUye&#DL, |iLj*lWubⳒ#yz3V!kUލ6YqQS͌~*b}q:O#,ú*Kp'褐$ r_sLzkWxAAp-E5 |ZQ\̼ht&k7`=ShrOX]Pؙ#e:a^]kC7hu :~Dʔ T؃~(6u%-#@x3^Ll$XCGpgiN'X1k|GWpֽ*lj悿ɎKfu5Pu {5H'Bd6 rccy%P퍐I[(ړ'T!c5$k?R/[DpO٣8{XrSG8ULУW󶬰HF%0ĈMv;ZRz {2 B,@" 4+)E+[v=Tlfz )B  `ũBԨN Q,^Gzɬ5Khԥ/7:%= ŏʿG귶ݸB:Ap߰5OOsV٭)+E %VܖL)t6WT<?ُQ3ۜ. Wʺc|C}P c7E\=鑤68F OggS@og(2HH+'*'KIEGFID|%}L'nYe< =إw[{\^ΫS5O?z_l{%u>8>Q9i3a=gNJe^Owݢtt;K?SS۪oFT)[Mu]C}2#" SWL)&_(] DeeCXaʺЁ)΢A)vXT:OnLTooz?~K7Q|6c0zыR~j&l _"0rVxֽA6-w`r+9eNVԣai<Ps,1z_X-QUsbpS%{|dxԾ SV`. } '9{֑@brW5ss~ol==Go)=6x*:+ Ium7vHK6Mo Ơ?, >ɻjr>b@P0///Jb9ei`'ԥii_uWS4drG`ߏV's.&J*;jrMctnT0{7F|7>K_$qhNu8`VS eȖuˋ"3΃9YQ15D% 5߈**EZ[?t 0o34@xGA7~kMM (fi.M|%Gူ]V1`s>k39c b,"×PMF,< =\8l,f {7R&w.f}(Q"KUJxJOJ0'ŖELFpxY0;1, ޏ hK`eM 5ð8XCW*^^;t4 :0;/·Zpa2ٺV#e XY1al7V)rn7a4((8qPMO3yG㽼O1:j{X oj|XI "*ZهX<]&]ə 6R Ě-pdH%L 5vh}wgt'P w쐖%huXiTA2i/u~SêcX8b6%E288>FP*mP' Ґ$R+.0R'w8,8eIs &b4)r(Q\MMJR)dzp#3Of!H\HIA߭!A1*Q֕ӊԠt8]sp3W3 LRogb*W;*?z Ahqwf4pp hz= ;n xOnuL|| vsvy~0u$~|]08u$( 4ϭu|{do2kgOZhsS[W3K: 017|OC^?]0z`V 8l@f&ywӠe>p'RHA*uT|[E=JOqgdߺӉ%ҕ Tvo ظS֟ҤVWR=}| >G?&U2.6a%RO{n=!P?s6uث#YCnRgqXbMĞ`hA( ܖiRmՓ7L hXRy>˙{Sn; ˍPP&E*j>^(GCE̽k܉D@HUCN^ fbFoqUio>{OqHazỞd6)}̉m'RD-= @2E,XLZc$F@|\~ґwNWď#I"3oF|<,<1!$+9ݿUF&!C/cpҢ+,]$S0$ocp/T/&~ en3X5&<  lW ?ߖ,*P6fyaܽ8ܪ(kE$R=[dk5 Cs^Qj5v*ۦȖ2eɧ,Hּ*ו)lp'] snNrS}҆XP0t]s!ճ.BE0aԹ F_f*֒,E ^_;>@;CIVpaz)ߌ_8:l9]>{$}Qf3Dn'V󻰤Pb@-} 0ls%h9~OӱRVV}=YnbȘdyoފ;WQĕ#eվ렍%8I6eُF5x P"q=e/wۃeX5m|K2m{+ %+?fɝf:볎T!AϾgG'c B8[1 '@Ty*c&@,ϔ78͐ox<15?8@ɹ)0m޼x9ÇX5|M( # T G3E>|lP7q@dcfߏ;tOggS@g( E8)o~|v# }!lwF@pC*V~' ~R3_/JVLm-˵ZihDnk[٤ X%MS1rEL L;wL:cq T/-ͼ̈́s.48i5_\i_ms!w'dDYz=JG6V?9lOVHp0/vgrhW?x'߆v޶,ڀH[%3ޓVfN,lqg7 Z:q ,r%"C1&v'لFGu:@{亼P.z hbƁ6 ӈS2Snt{S{yE I}Q`&,Aʼn%w߿ެq?cEzB__ iNs8^' Vv W(F߿f/>ǎ] #CgvL3)riNv9Kys`RvmFJeb;g2hmR=5XBrwquMt#+G9ALJyey3%PDž8\n <')Ղ}Ԫ;nͬc#Zu7օ`~|_+d'Ё=$ۡ$K_gվǽPx)ʆEj}W'?Ey Ch(`~ d?-{~6+sY &AElujUnvZl{%*/Jxu\Ϳ }DJb>(9wl{4WWʜokMOy:ާ|FPq\yDh(*$A?7wΪ7sGni1צ0<:QtBЦ\*vC5v[ G>ý:4dAF5eq*;F 87Vٶ=WGKVOlYO:MDw T;,J }Go&#l$9ځu =M P@#"w&Xח'k9fN*'o{7Kc]kntWiX탽=ш\bnፎ|̵HwkJ99[;:G鐽SVczWth}v .ȩ?eҮ /`9d׫x[o)L0\-#HRonXM$>_zwD@"m 2-Q3oxw]+r*\ ߍbvr뽵=Δ9u4!ͬ)K$0YS{(.sgNq{}7: ',F6'amNH}F MiC΂k"qה+yM&)K1tMIm @⶚4ʋy&][U<>|^z0x8bzb."w'bZ}UEebWB6>I6 꿷t'`fbeJx輸-C|RvgeƩuCg339E TؚfJԂ -6I 7Ǡ߹1)6mS,T=xԹ X z"3#`,*h8~΢Dl7o+l8ؒ1pjt7[@XYAfz<=nHnF18 \) "\<% k`&(:uʥ8 [ ꝔVnA$l,uE;bO,em{P$Ddm>szy^(R,(i)8gNe,^|1X=o]G2iPL o4ǝ62`K̋n;|A(T>shXZ/S`A^okJ*nF|quk h,Q]nF&kd'N;R|`E5';O %*_uQ 0 Q4KϤ”="ܔ;1_B(RfI1*VYp|I_8 6Yֿ5'H6OʹGlc KvxԞA$%#N`NM>ُA)>.)"LB`mq4yevլV~ X-jꓥKkx BLKAv-#3 3V:;{w ]S_- x MJ4# `ݽ^|=[k 4 mڦ@`"h$:iX~TaEUO~r{\4F+a-ldE )~S:{@ŕV7DKqv7\lZ2ID]w[lu| {dH"idJѾ¡wٳ0ܢ?߳[' @6-LmBgפYlTڣyϑQWPoR,sq@AX 1[G`#RMr>A]-;d' דÊTo0lgȎn^N~>m.еlLhĖ9 #!Wn2%T@ 1W^?z",qA?D&Epv@]7)Rstq$ARH.qnLi#EcB 9j13N7Z᭤Dϵr 4H; o(9|aLDY]Ũ'+.u\LB 0("G5[y\<*Hk9L$[?8Miq[sL+wSh(Vny~m+&ߔt B']\Pj08ܝ hqtg+2*g3B 4dƸ*] 1-S94 !Y?&WGK](Rn^.E`ȀI5HdMK4 ہ|pls30F~츏|M1#Gg%4n)XV(wrS,\dhsM|a\Jr׳k[Vu9*p &}̚gZG6o oz \Sw_b5MXe>-AB (}OS$3ؠK^wk^,;N91#v[ʶP(`d f[AvD+4VOQS-N܁&O>#[ !Ȇ&[^+]Z@,c nPNj\ ّ7Yp2U ^rؙMoUh[^|^g|]*GӁ|z I$]GRJDhRvϰ^ ^s{-NyGdw^TӳˤoDCxO8Va&ׄFi֨%R^iw/cBHJ-h=mBM!>ȃΟkZeZ[QĚ2Eػ5|.]_18. 0OUޓyu(5֗XrQL3OggSg(!'6*,GGL)*,IIH()FIH(LHHHFHF')'*(&IF3~}^ fx+"̫)ngwfMDbvurE"[} |. ~6#c:=|ASL~ B+^' ()%,}*䑠_y7/6"bw~-m^q=᩸h-WcKzVAw2CsC\   us/wWSA+aA$S'v*=l kȕL{xBذ!X+(Pr{ QtHe}eaX*.aq]yJeT;1~~I>!ףK(ICx#YBlhڂ#X)?ȤuM_eOhYҡ( X>_brtu,Ln,(f37}7jwq>O/6+JէߎYi|V[?HӇX*;Qa)rQ9e E CWsh"H* R* @\77 ‚VgzP74-ԦXz7adW,@-'#pc\~"@G]$y&T)".Z~۠9#:/Zٻ%yow *Po`-u]&N&HQgR<XHg` *s$lYY܏FU pyeILy pKuw4Qq&X?U ?.pe7f %1s(1(nD9شP=t{w|\RB p <e+Z:XH%b~`J OOqJ!< a 8Wja[ hP&quA":$y*Q8lVyK*k>YވUFWlN_]l^ {vYx(ӷ.:iEq矉o5L2p_X9~->8 glzcTz%6},2 ӛýbFk]iٮeֻodBkH HsQEk P`Le*@`<'o +ׅG_4R78 ,-p-* yqb 3 !\ duIX>w%. cv$`Z*|4Ic**] IS+!]"Qv ^Z@E5Hcڃ^G!2"zIBy'KObBa$@wr0O?G,8x}V؜idct5 W6ʠ6R+$h*k5C4*at!PD:kdDIڭp_ {}|`@IW_$Ta>=|뎮}c+50q} #"|WPR1Gvj5X:'S1G9=򅆉G '`e2h:_O:\5'fIօX_&)A{CAпEU447{s[ڱw*%oSb&Ysá<d% M,vW[mW_8Ѭ9N_oq^W>s茌ssP}%75r3Sl@wU=Bz}˹!FnV8Z>)O K'P23]2ZTԓqy+ 2 69T:0)n"Q=/Eg2C(k%gm p_eCӻ&ۨ#]g3ݟO\Y}P֢?ͶoT=.Gt5njia=Ͷsvřo^6*7J.Jx2!řق7'>zH}i(^НiY,ws{k9<ӽRr4q)N=KN-1?gat32Eg)kV~ix!řix=J"O>LGc__e[WFo =^f0[ݺxly#A*oӣ#TU:PLWv t$vF[d>om NujZ~>˺{ `ޠR~ yR<,NTYo9Y؟;+v.ˊ̞Nݻ=[c^U?obCRL:6ʥv!=F80\a_}%e ȗ, <6e \.=Ab䨁ECqQjcRaaHSIni\*ujGPhXyp$IpӌbkkN_WL:wF/cH^= SĒB#Zwn ]o,2gNʙ>'%(@Ș8v xr2A͙(?P` 5w sw7,ap_WKϋ煛*|_1@8Uz}nej̜ir z04#wlշ f mb֧)MB6Lw֫^vvδU 1k,Ze\RyNDWYd%E)@!K% +@s!Xvf'D>|(o%}ܶhq. ?L,02إy_v6!ص=GvWz+q! -2fI[>ʼnIb| TlOMr >j-WC~'"BKzjKg$\|19?)$W=1.cw9jJPBE>smK pF2wT;g8lN_Ƕ.]4|h(|l $~KAtLD20x -1ޱ{JV *$6]촉BF.L .yOƒ OEI;ǩYF୶4U6P39Zʘ6@GMktE4喪T[nOwnP['Û=p&l\t>`|c-hS /$ $Ey$3xn|з~A#wOQm鄐oR\&,"낦jnVq0]If)EPFRNy;|-OW2VJ$I[OoF\obeTB8X0m",>n4E%$+ئGl!w ck!tǨ`0V˨~9_Vx kQ2Iw)jA4B-!|%8#/WJ E  W'/7*Wq( ~ UCۡ,W^+@ TiYKnY(gHbtʗ} o\~3!֍>rqt&gU]sB!Qܸ=׻[~-i!M{XhWŌ`JJ%҆{rC>)Vi'u`bư׼c VAzϣIJE)|mz6.Oȶ^d5,YOz9w6N34*8vQ|['Zfq䯦Hkճ!뜉9 Bal;QC{FJvpsX oBYԕ.=>sZcMO~uA!E Ǽo[q'-eEa@gsKkg6z5JE8G*lPCe*J!@ rWRʑb*bBlXpK%ќ*D\2vyFS=|8?˽~(pV/-g)~W$@CX_cvZ2[ߧ:PIN5TԺ+=Avͩh#Ϟ[E$ڊ _އLI mKU*$[ -?jwx2feձv5u`pX]E7׸wV^ӭ f}ŀ/Bk`u.>m{o-ffofEuNPa-˘I4@vqa[Hg V2Inb ]gVӽjl&IW[]hZueZF5*YN/"obo퐇{糆BB2^|][2QQ$j f v%@Tܽ1Bm8+?ѓ#F!tSq9ήuz"' |ߓElkU,}*U†FusM#fZ>Ш RֿBƤ`8f:󡀜VzIMƌ)W3X6(XR@/<ׂT5rW/ߑu>ik)i`sVk+%HeZB@MUՠ}Ŧ(:e$tdsyȈf⶟fϻH)kMrErV.kˑ9fΌɘmCcVVy`kDs|j6C/,\w|l[ia,`~̊8d~aEgQ{lmE*.<# I,Mrki< LO2@DhVn;hFȫ6|N}AG TMy!n{6i<;G8JvN`7gQ"qE/vqL,Cm |^.XwA2x1b `̫0jJtNb-.DYc;,@oOis2Ag>w'Jܲ:\f?J$XTyiجI#bW5Gg^Fp;6gMe P֔ƭDzzT1-jI=}d! )?;mCwUr%rُ>/:f[~|l' &X$z^'~(E$S*sokgqxƼv~Z-Cݑ`XSWN9k=C4ZY {}k!QQz}r=,t 03STL$X6k,Ae)tv(c[մcYq oŭtI-/[q d%+߱^xB{kʕL:X~|~d@䦡E$YR܊A(77tf5@ԫۧj&.+RI>:nœn |S/:mo)6IB5@"ţq8J#*ߐ!6_]?OGQF& +~ <贜7R1.#J56 un؏7(|zTżА/E!$)2TWM5~  WeAďv3!MUI~Kի%j{D#qNX:-  _w;P\IOpt}W5Vz9wt"0[W^o p)3z̗l>Nν7[޷|?h¢/N]I*)+i:k57[+:<(':jHϞl lb0bn=tMSh\S4jLҒb%p/u>ĻZ:+D5 ɍpOr8ʍΐ*{q0:yO 50qGrdFW i6 ߶ԩ'|o(:xOggS g(# 8^|^Z w73D$};3Pe{ PۺsyV_o[ZLd/"s&`WRk;Z!7w}+'Fgk7WR aI$TyjbqA_S) 2Ali.S/Vg vf]]4j+f r"y!`~|j6@M8$e,@b l':)~]2h_-@( ?BPR! <+gJ&!@\\WvO &5!zli& L@MXdHݬi5(k55iA6-,}vs$I4Ē^նd(@/,y=!tvaqy4{Y +|\*$~d_GS@mYPLUKbϝGA 6UQbD5f MyE~Gahf4 H[\7ye7AeziHZ| 3OGKq2t8[Rq|}W ZFv£T" M?}ts7,Ct> 0ۥj]@ !*6LQ56-j|?zr $ZZHf5ߕ"0|l'5`K3kDiMw9#d9U4>Ct77꘸᫺&yI+L)].23⹺6UPsu!+7]~վEe&N&+~\+G/T:*ťIaKR y; KqcԖkܛ0}7g@UZ~>|]*$XHT 3I01'@GH`-9uU1n,vF2>@15%| ^ב s}6Y qޜK$Xc‡4S<1 _'t4׈rKӚ'ǜ\tpfɑHpyNFu)RA'N {،z tzNXOx~|& | uIW2i0kxgp߱o4ˍbeSS+E?*BX3}TPNb/E~V,~:n'3 sR[h!hV$5:B{&ݕGP!RJYϕIs)X6'pt0ˁ% +0)q%UHަܞS/'RN"):@fX_+uqL{\\hZW#{ZL ɌyS|b*{'F_#4J8 ,ƺAbͻ8\곛SC74]5ۊ$uo&"G,Sdij6ԷCSZ08n ]XJFe* ԗ&X# &j0H&-ߏR1":.ـ4λϓ%c(#krWQ8DKkCAN0W;$+LC_blri7ftW_[BʷC&,YWcpCpEn*'w7j@uӭ߼<lЮ*Iq[xJ(g,P&PC;DLj >|KMDy/LGj#ѻvbK `:~򏣤QߓEw͔0;}_ky)IHRMTKpTD+u)PA3Y)7*y AS4_g₫dXDoVt\c/zHd2r!{QZ*DmB1GuWGZg};2zt'~| 0tڃ^IT/MP_腺OcEkET7k-H}G=gwpL4%lS/\µRѕþ%!s1xp'/= '#A:/"\YD۾]ܥN45ZPCqdM4`/@ƶOd$:݇dntG꺎.z˓0u="6 TJRKS2-F32a__E~_|@wu< ^]x^h撚o>| $`-5?0Hfd p~#UyGr&BܜS$ [f%>sW/0X7mNXhG`ĝl[YݝLl\{8TKCwÍJ8=TmMÕkwXn花V#> d*&D7/p)[IXӮ}:@5dd}LKgk7^?O'˃OV!<:Uu*grj[i!-/ܵlm3"8?c% JBXŪ2){,}2p(1>5|TPiHu$"|א@clQ㘇zapoeNlh3GBvoJx}.mWl!lTg(Mx)bTАye#tY}A;ֱa<ݳ _2 )#Ӓ腟~LLe鿑GXQ8 :!2XEXF JY:zP}`i2{yfB=:^YT?& CbԌ5\YǞ89TLvԋш۔ hpZo)47r9%(mCEu_&^lYExp&pD[9MΌE[ڮO\o_Dr*iZ1o{fWbsSLX1 ިd2܊ uᙦ>Nƶ}Ȕ03eQaMH!:Y0'ﻗ@!3,4g"睮< x{%=8xVrf^ns+A+:8(ڞ pZ85AhHlHEY6a80W |@Z,p]d:3M$y03Rqӹ݁x?5_eFABvj1FT2"pkyU"ŠlE]mҼܕŽJJi/bȪ J?eҲ<wH&Gv $Rt4vP9;C8Fၦu{>|l 0M7? RO2}/.?NrV!S 9D:|FqH=D%T6ۗSVܻ=]rH#uUmD OoԳo*ɅĎaIzً].forn&IYS; :MxR|%{3:|^{ 7dDst$Юnv#pt~UlynY& 0!` .צi;d*I9 !")nv1=/ l5﷮Z`3 ?wmƶUf6&^AvelT ш1w|;SOwz>o;.j'2vyjv#BZ&.ê _F3LubZ$d8\YErZAњ2b%HGpr߫~cLiYٱ,Қ leHl2k˜,Ҿ5Ѭo}c9vo dWSwvQS,`}r4}[=7{Ht*~x2|8+߸މq'RpàGOb_ZNOggS@ g($xb"IKC*)''EGHI+)(*)(KG֧ܾZ""xc z/[D(R) |bKw1YcmDe^qv~5_}.\yH]IЊkfje4/9i4D? ˥{K벻\1fc%A~AcDWiz3wz(FFEuYM+jUT9`?^[: ZeDJ0/WH{3% GkCmEVV}τF/RcsS}0JΧL ,:=r3E֣֬~Y"5}BVȹCT2m=ֽo_ǿ1_oqsKǿ֎9;1AB?I!;u@iֆGu{#=c.:^6?~tv~weOv "MI1?E^F !um;{e~M6J:,¨nYɈ B}71o)#'p!W\>#4_CO&%'@2lJbl{Kx&I9  ;@P#3J=wv'u,/5*=] V;Hv}XG0'esRV<{E l1L %`1 4Mj5ǝRTH( ; #f:j tcY9툣^wUrԬN.?)2sRi :B:Tܱ )qTKc}wW7K{ogL=.)WvQ]Uxm،w9yiٕw$qN>iD{(E#7eTaA{g8: }+c6\wQ8V[@k4YvtsO(mA\U/Tj&STR#mӜ1oJݗs 9zdIƂO|t{aZ ̲P*OՕn+bɇz/"OFi=@7ʁagbcX,~DCp͢K.m @RZ$S 2TDz5Q[- NqXdպJ1X_52昣ƥWOFYbRoB\{tO]0 p 랶j}=w ʺ3w4ECij]@ >։}~ as1Y<%8A\ wq| s=ML죁XdCFEw:9/;"d H@=Y\S8-!Ȫ#>֖븈Bb>yTژ`X걾 :,0b;"Sl݄p*r;&RU^miTaJPjF4O-i]^O˿*?2!0o,7Pީ`ze0 ާ;?h fX@D YI `e\Pq*si\o# vt{;[+z|)19a?*[eKCy2Mxj_G7I <tءوFŌ/Vi~C@ C/Eaw%[q6sg-As-#'\ P"ZVo+|<@@~:Ŧ05Y)t!wF ebOaU{vCgmM!5ORWAB,F93KUf+lZWz:7Nm9;[f*N oZ'd$XIt5r6Wwmpct4 ~XsO9Ʉ[?U F9ڛF)Fh9+8zoDs]If{h6m%bUfS9S5E.2ĊIr֖i'3Ob I+JmBs_ȷMHK_k!OR%o7$wn 맃Pf7=A'gIji}&.iU߲?ֳa9/޷|] :HDGyJll]ݮR@t8'ڰEָVю!H=ճJK9⪶6d^j|gٱ:&_cQAhmG7v>[Wsy(PL:^U"je-U"d]I QTO"buKb`#"(LLNoSdfʱN[N繏|?Z$$RL׫0y쏽,]VhIuzsEVee(Ԅb؛*KlWZX\G{!}ۖB|L'G=ѨufYIl)%8&GO]C0 JY˪OqHiPbtw(%ԌmK{k>'TՕ#ŪqߑIk::`hu*%7l{+u,^+,,k,},>:>lIJ8:OTHfdNm^{>a]W|kM9"PrF:mU>_coҨRqbJTJyI0}'&C9X΍*Cκ#%bb֋>$^&QgQcS:325^d%>| 0?!xf#Q3*kc}rѐM [Pwżbơ 2D rXO›?jv@ψ`kj:Fs\v맔t9VXy.6In=r)! f."{1.D\,3mMI*a'4ɩ7;3jtB$+b.Mёg|;TBiξM`r'dwЙk_$7 W>L荨2VbojUv,ѩMjgÙC!2x}r\{YOggS@0 g(%wmz bZMPDT&XNwmKmC=}>[Cpa3]ᶷx52ѐ5/5J*a% O3:;FP7i\\fmDyo"tzQN+,,) Ns7+3H"ĜWڞ $u }ea  /k?"|u^X5?]|ʔ6g >]GBO MьBG&[_mUv .9- (vțK0^wgԀ Q9FZƛkۈ.!ߗ&-6(0tU״u-^ R%q9L@ЪD[[χ+1g!" +-FCiֵ4touX>k K[UM)I(i.ȶfmRLWr|++f˧T8_+(STЮPGdձT2'[|J"Y@nepM/ )4¡Il<] OG$ILX 6s+~\#m a!GRR  yw)z6euWx5-p П`}AGXF"(3v}Ⱦ[%BӼPD$oL_ՈWd] >Lh s?I l@q(cWl+fCDSMmg< #,呤H3yqv)ZiC*kd -'W F#z(k$Nѱ}1 (^'nӾ a)24=D2;S[vkl2~Q <"sARq5MC}Xo_‹͍Z]5= ޸/U|qj"=NԖJ!|$c>|`~~C7茖ϧKL<"Y@#0ƍ#Z{[|큻UyҦѼA3Vxi3@s3,*uGghw]˕J~2̧]:)Yhl}G~Dpk}peYs58{ )q]FF<X_%[hZ=V`;1Ԫ(-FBb/.f?r`A {zp48OQLR}^p(4k-5`oẍ́rGrf&?|=4va<~|\F Z LL7XMf[Yܭڗab̑-;t[9N։ Hat$^#ޅMUӭY:M@`ߓ d{qٮېJfTCqO{ڷږu|(Tta؅*wnQWvBpH(㝄5Na5;έ*^_R*HiI,țd٫.E+2(kWc% >}M= _[; ")yeN-yʂ؛5MZglusX*DUgOn zFTKlL4݉fU GayH)Lgc9f^fRX}Ġb~|z7N IRh|lXOg_KpnU~ɩ4赚 1@c *9:"nz3U]@SuaDsd%ޘ-\NQ s]w_A*BZ)XT D7=Z;,S'4E%ח{gB[z|hc(,`(=ߌMtL^&a6s p%$ho9L/2\e?$r#}BpCש'A̤f:(|n\H/:bV04aԤ-<+crfk.+W\57b0L+Au'45JPDʂ {{xi>̡,7]'oI:@s?*ɂ`Y'>{g~)""B4XXfENnL+i(IKc N 3n:J^8>cꁷg,yO;t&eZP~#iDB <\+`L' 8*f~!&KwNe(C^|Y[qkoofu~v vG>n'0g@28G/.'}u>88ΟFZ( @0L=@,ς2KBACT|;y\h3Sl':wm+%t,FӦ/{387dKy\-Aùq&nW6rʓy(e[ >3g8,[$x{0:,ybWr;{l=%b{Q~Uh"\$pq-wxr" en(7s&rO)<LRBxN5$bqI`pDNՍfaN+/ Ǝ#V@pKQ+0>@H[i$ku.2,|Z hm69Ú:?WIM5h9w~hc6N_@xp0q&t>%On2U# ?1#8R1oŶX&rD\^VS_8[K'>qBJȻjvSjt"K^:YRCa+56uvᝮX;P]Xi! ^>6# A^w|JA 2BB:fۢHV&M$' ~FKҮt_s0KY[ :Y<.[5s+fQȞT h\D#k׷WvL/)`g/kcNF> v#]Sd=Yh15UO>P%˂YQdRIޮZ'Wz!>&eBK}$ y<Ip<x.!vWʔo]<Q?'RF͔AD9 \,.VAfTR?t=60>FiΥ3;:",Ǫm/, ;C1tv d'sɇԸp/ϭ;iMr|{WOsV 4H% j :7IieuluLߏeD»f EFG ъpZt 5ɻnT7sfmx%ua?^1s |%\Y\QE)8[cG|'=Z:=eZOJͫBUx[ 4lJijH7G.5EYc}.KK]^1 A(TNT~[5Fjg/5bkBSÜ[1WԇQoSl?YG~OJK>Y)KWG%$ku]vd,ۘNM7X=5 =(-}r*)"=%7,%_e[ȣmb8$FOggSq g(&r$(*+*)*KH,.'(*IEFKJHD(''.'GIJ~)Y+"~a*dpry`zWɥ햇tpˬO' ` ዀv0@t=9Oe͍e] ?+IDwC6R6Ys_Hw#Wmu f>qfaw8{ ^@F.=A(߾X׵nqqiFz;f5@' wVH@<г(ӏSxYg__Cnf& !A3.<ɆH?:^ ZXVzoG @_DsӟHX[_,ֵ)7J іZ$kj>)1ÁBg*VSE8Wnњww%qS}Z1!qFWAH5n r(j (7H:}H٭-Pvł=rdv >&^9}^|;>p7-<`à+uCjl;;)vc^Vp#*~|7yw -_,7ind 2R"DhA᳔O+:y7 $[VHS[1 PM-aH{dRJZ5?㳐Lo"P `#gG{Fip.vG  TdK`4 "%cE ~*: kk` RWDIDZ`;q GZ0KqsܩC$>~ixMHefLs"&I覥 6J3\K?ӈhuFՒ9?(r6biÉ(]!68n#_p'2TiÏ;[Z rnN @Jdd.MFd/iqOMTU 5c,G8D 7_Ba{TÉa4ŝ>x>ѕIj|jd}ZR՛$D3rˇ ݯFɀp p&*A3D~b"`|wu1?(|>ԁ&5U@+zJ@GCQ%ml&3YsrDl/(ҋ(]/<ېz4xof Kݲyjo96wUe â`Ƥ|wNNe\{aWl .?L;Z.-,1Z֭ I{Ɛ)-fO58pH)ǹȍڢ,M#DI>!|Z/ȍ?(UwwG(]-}^B\Ɉ1bv檒"3ߘ|#;B5wyT31Q"b}IV v 0wGR̦A-C"S4鑧Æ0]kݾO)@wVinwvXlą 8F[R(Ä`wyW.#LYNZhZU׈u5BSD}wN^ͳ! r˝eoʥlPR%6M6b,4L[q5W2| A:iL@a=P =q@YEr)MCЙ][P~Ox+_{TNO1qnJӭZڢŔ,>=m<IS 2Pi\[Sx$IwgM0X#ׯhRuLAVE c='4gn~gFtEm%ؓ z B$è^?fBn/pE$ TvjNg~[y|ȹׂ! t Ѳ2k#hv9Wr:~M=zCAuѵ®/fyESg4Җ )춁qw 24IΑ $pzL6%"'vp]ڳgStn]mYOP5zz  6bv2mW~gn9wL$!-Hן]PMw"~$ro7>֫[#̨<< PcqVoqkAD' *I+飽 ;[GmH =kRUvHt)ROFQc(8>u |KfgU4w=T;iUctizʜfI $ &J}"QcgO)izw~&d}27 $2i 7g< C8? g~y:_~q5n hs^Y( Q!$er\7egN53By] zW]/PvO-M +I8 WrF2u 0P4T,Mj':|krcxVƍS¾{UC2`I|W.\o<>l=%; )[M?FӘJR"M߹?ͮQբuj{ϏtflW|: G@Ѧe8!H_ `AO͋9\u7~ՂT_u3Z6=ܿuTZl\ضav{r,ݮ|WO<<^Ojq |w6:ƣ\spH*Q 7U n+m Io^GLשy46f^,{G(P#vyL>42 l@au~phKG?=֌p K:Olf'f^ i0er!UEXi 4>uB?q~k?L#V3{jy:McZ:$}o1EJMx48fSkx|4 WUE X=r&oCzyfyoYf,(+۳7B],w꽰 V)7rOfߢΏ @( Hmc%-~?i~:0I [oC48nU0zl{U7;4.R RhM㪪# oFr!7yۢ}Dm0_q"JaAdEp-kRZAQbVAK{ި #>b, U 4˃.2߱{u,SMM@M7<FTh~d6yuk{=y\vec=H%'|YT0#*wCvZA)zh  pE]CR坙BA׺T@J2ꈙR=͡1[Zjgoo2zB1qfbgʡQ%w^ I׋d> x@#DbyU<۾}ɱ ΝDu~U H"xV&̜=ڜXf fE,nwMUIH8kfq6Z|,@ͪVs! w0&6ÿӂs O)Myz^*] du[|֌>T _|}+__i@b "r -|<~|mT*! RC?: F ;V,SMjh7#5GA 4УP ww[p!HEl(Lo Ċ+IN8QWHT]yewH͝[1=Iұ;`;pg5eȯsb w$<# m~O:݇+V]|qrޡN )8PQ3 [ox,3Y7Muu8rU2uZ%,YfcGXJ#$YwN+ƺ5s/\7k *R$e ЁRֲzW2bMՈò1Ƙs28}'sboGD(=XTc`_/ |~$@]IDX,M9%U?|^ɭFr1v7.==0DOfm؜S |OନfCTmXm*&ڭĦBwr餖䱌5×p9xYԌ^ΐHtU,ZF[ gSѫF4mv'-i+1QurI= JcKKBToj]b9+90à޶.#GszdS1S\eL*kqTC<#p61)X:t#(O Wᚧҙ p;~Fqlon}euLJhwKn#5*1 xR,M[da` 3^W>qZK-FPiZ1bͯ@^|w8L{$~ vم̞[vãEŌģް :`b.$6*zmUi2BY,DrpkB vQ+bG5ݶyΠ/_i⎫?mgIZ_ &2cgBD22FkeѸ6pQH+ 2DF( ϱ|]:o0BBnzDJ 2raJ-^p8<]WS"EsUN3 8O52$ѕCTa)Cm7\([wi{C7}}zz  ,!v fȱb@|qXWZ=+MP3b\8{DpE@I9ѐڇ4~|\?vlZ`Ad+z=ZRҸֱؚn}cޫO)v5W?IW*~>DY82 [hϭũüf>;'n,d5huȚ[=GVz?VA%ϠU3M!Sj1EVr0DPTv'͵^|\J9 ZDDjkOˏ;oGyWZ X: 'Do:\\^Tjtu_R8Y~fBPY߽Ts-6A@̛ b w.k$2`SNW094ה?Tuǀ'N]k`}4fj|nk;&hX P IM>huM/m> UL_ dNn<+lSͤ=IOՊsNew 毎ˈC0&&焙A'i:NFm9Sy/J̄8X^G:5`x6M :xaݬ3LYs̼> ^|l[xާR_ي VSm<1e@}EZ(Σ (pg =#w|i`"`l-~:pejK7&?YKu%uIMaZ}1AP5-)p*L w.JQk2t^Eus"!η91 x>-qAQzteL@S}~i$xЇUTciJ_Ô6Av<崳A 2۩.̎IR#y| ]-%>C/>-MikcEG[aH^f;U!f ɯIMI>[jg 8xA5M ?;3u&q<fm4aآ%m~|Z00#X1>HX3\ Sp_L f`WZJH"[=3WF8:5EȎ΃;O(Q;6t]6SV|M~1ֲ>S^HCW)WA>e*'rTt;i$ >)[+N45!8^|1XAq:\t,HH ݦΏk^=Ocv,|?g*fsЃARR0v ̧ R %i-0 L9>q9ox7q!! ҋfcdg'P@t$|oXv~JLp34vq}(#f!,F?~EM5 ck~|#>{$,KL"\YR/&ZOVpukt9}hҌtWTe>sʯ)5mې,rg7o7k^|^^"[)Y 3tCl6qjϾs <LjHD~NbߺuE}+ e z!5?"ytTatfp+ CZW$A$_Z.˦ޒK[ڢ;ԣ'zűPO0nbcg]0ЫJ>3ט' S"Se>=^_j~rrsz;uTGHŋ~;2uJXDIwB4WCOM&jL_"m"FEnۯ%Sru-A>jI x.&1ec7Ě:Qb GO{F~n/]A{nj{&W^n]9sm u6p>wGP'g, lkgɍ_pta_bUNb`?{M5EÅ]wIU?8y[O="p#}19YL).U7؊j~6[*y J ~c--x N 8Ύi^.)^RL'\_ ۱<5chOggS g((т  q<PS2 dq]B9AɀESMV)COhښ>n: &2EW#;Ÿ'Ä2ե[)DfW eU&ImDK'yڠĶ@wcW]h*im LRYP䄸n .څ˜w;@7JV5=5>|^c Yg<;90^3娅hV0"W}u?П X v CsZ|5 ;{ G&Dɤd |{^oKwK`cN 滰Wu.aVa0a 1?|!.knM91,(~  IAejwpjL ?M*u*$]%/#[AO,H |ZueNXN[lv^&жWsnjQ&ԼUi;U.OhQ&= :9f8WcDL|̘KZG')зG!8 rNz0o잟4=d?(h&E/3N}BT`WSu6".}Dܰll}|nSIK0 _a1+E)6hZk&a]ѼZ36|NǸ=>>X-n{tऎ$^wsCj',JMRf,mn(5L.S^C 17W0# W|) ;2?2 3JD? *Reip<~D5AŤ' bjׅ]xX Ϙ W% wq{>L. KgtG)Aʤ>̫|]a+'=Od&+1ኙw|Z/N^˩|3V=3E&s1̧W^c8*em*܋JۡLmBֆ5~SBQ4IF^d>K4"a!"kу`V欟6?Zq 8@Fv%i`Ovh6uyHHHl4%o-Rč1aLd_İ|qu\Xh_2; #WWKniI* Qӱ4 3Z{ gR$_]Y޲A7V ޗ cTh D@37S$)^:*QwyHF6tvQKBz2`15z붵$"K ٍЧM8Zg$1hw}2Սo&,[Фٜ|DuPY 5>4YIGqݣMFYzn<ԇ>7i>~ ̳F^-BbҖ;7PocF<8w{~f>|W 2~$PWliy$I* @>W:O}RԿc-sK!IieŻK.GZs% 7޻j4CbJPp6/ .b^ih5KS|We3ƛstknUڼ^W6*%T-{e$25e+?sVR# ne_{Mx~Z D\dF֠w8j㷹/ch'l! q1p?]fÙLj"L'4h}Oo)$<8*d5 J?+qU$f2ʖ@x JFTf f fhd.Q\,)_.oBaxy{/~Fxg!1G4Y3^zkpÄV|]&5 UnLISishd%Z{ux&&Ef{+kr3ͱ/X6P^))e:3TǤ2k[Ü bnV@F]tD^%cy]+"6pR4XhJ 01Ƙr9s9H) tNJ)=Bz!B))C(!R뱆N:k!Zj2(R=PRj){K%ZkK*)z9RL-``'EcbC BH)RJ)c1c1c1c1 V+j'tfdȥTD#5b%ء`!+2Q5^+bj, AAe($)XSȔRY%tL)F)BƔc)tZ=TJ @P` CpK(0(I @"3D"b1HL`q!246..tqׁ P@N7<':xH6hf8:<>@BDFHJLNPRT> "9@@OggS51q'M 8a|…Um8p Vژ+NmXMr>B3'S09q{Rɠ(Y߳Hye2jBDQCwId"BU & HK)ED_ԓq@H &,qBEŅDmlv*iqPӠC Asg=j; keܼn_(6Q)J久 f0;`Z*yD@Rb^Q(\ʡ<j6:EYcf>kfƌ`P9`IiWưt,&=ai^Y$@Fe/;"f`p,81 YY!!E ."dR KXŪ:t qMVx1 @Gm J|Q##ƅ_)7o tI JU{AJ5FEa ^B\. 1D Z*ov=4ذC܊H P2\_ +DpPy,0ESBe8"P!"2"&tΏ4[_ShcYLG[H<]~oQ^o2@8&y2uL. :qͳr\ZzzGr0$d8ɾ$y\Yi%L& ڪV A003R.s>ЇE aLuL2"uN ~S~;[4TTPQC3LQ#G\U޸<ב]BG=n>8 CTw0mSMbuLJxe)u1onvQ-Ѡ%8VP\E$1@q >\yа;9t?uG1tr'O5=r_bHsHMR$;dԋtZ1I$(j*e+ߋd wHuh%z鮿21]TC9".=D*Nba&,)sǜ_/X.cyKh0-v;ytΓ?ADX ^=oG^WJ'+գz} B\=#'A@d9nFK̉P H vUmP~%@$ 83{,zɶ Y9F/*1H`&˼}9{G#!Y|WĜqq:l gSƘ={iD,ZT}H`WwD<ʋޮ$UlC0fHXhBfJHJ HDKQz_m0X~%Vka6R*&) I!XAPJ"@MUGӏ;/Yuݯ-:@D < dǵB:g;2CD0" '݀RIm!ړoH_1^Fb6Bi@@H3`Y%H@*|bV2+O{'Mэ=Sjp>SĦzRMp PD܌ĸ % *ߦ\ncGFΥ!<י? $.n.A5q,I҃pCmD/rb 0'}^Līؤi'}]\3'4Cq &PfH 6PTG$H`%PHҕ˞eg! SRDUM %D , 0.B(|Sql:3t/nO9Ӈ AE2/H` D0K^Go2#&`sF4y$BUiM葋!2`7}]7:tW_$U&T!`6L HZ^T{PPD HP #^2鱕ǧ=k?緭['ڛ\ :x(-wVigfo˜Қg  i,F D P -i=y/_Y+L Ϧ3xI&;eJQl`@9Y `G}܄ xIYۣޯB_$*-v鲺;KЄB H;#h@EHI>q*=.+TzP=;Ku/ 1Rqq1 cxaiNo/:Փ#qVޭgPl[[0Sl0"iuVRazqKn߇µ#xšF@D0$I1AEC$Af `g}܄h~FW%ڥÂ%q8A B (bVI 3E|VbQ&$}JdS7隤{gzl|~l}QpOjDV ; 0X(J @;eRQQ*ڈ D޺ Q1;V Ԕ=Wjeh[c;01:WLLG^7X ROaT$j+v F/<3 F!HуkZ01BFQ Z#v`OggSi{ׅ }NKJgj oP @bUM@u @1i$ED<#T+.x+yCD$b%*5-uHN(V`Ƀ($o9bc]eS2jRt7xe[ԕDHN/QG]A6A>~6}.AP&N|fh vZ'Zd}QxoCCWFw J <|.)(D m M(9W]i ՊA5R} FWAu Rʁ9 $`Ef$:ľg Xl`G10jkO };;qںFٹ'(9vսȯMxFRn.+39pmL;!#;P†gt%wJb3@:\M=C ' ~Q QACpp]2ی!b1PC % f- *7ݤ qqQ7//9 asA R DBLJ*L,0EGg=ڔMMş(/Ŏ %`f|aV9soNX^+{QB:yN.;nA0c(R}ޭ%*r(g4z=SmVߛLq˻Ж1YŇ6 i`7!VQokacy A?801M{(+FzF1~7}\ q:T޶Rw]\Oц"Rԧr> )i% 26k%hH(j].\uYk*AGSqU._Z4ef挝Մ a^3RL$(?1@D !HM6\{tsNb-@FcY Meˎõ#Tq:Kʒ,5렫[Γq#(+-boix BiAb@Nziy aQ@o^7bʃ~z8[Y~~p_ F1hHOA+   i:1`2D>l8K:M۫;2Ѓ1o^ 5@&JP ID4!By)jMJ~uN4i`2@V*@OF%d`tf q4C]tA]iy]Qy_ű'1FyP1bMb`P1 IjGVɓKnMDoAJ#쾃lKb E ZJ!@ahm@TDHAk# ^6St´[DY\Hs)J@.{Dqʔ@HIk I*jH"E i@`MrDzr3=v}q(֠M>01K!abxw1R؂ߞbm=:== 1bVWZY8s8gNfu3+9~H_W5%Tv$շ Vtd/S }Zop $!` (4ne+ {^/ReKʈ8IƩ )8&\ 8"$B⃸*>c$"n[9tˡfd_* ,O9peze>źelE,uXO ` , ^ث=s UՑY7 .CA@LSoaqKW5E`@)ΞQ?͢NIjZ c*0ˉDVƈAE@&zЄh 1R*{l:'!4@HG#H I %@)=*iJR+= 13)LRSZR h7?'0j,ﳻyJKU.LO_.KuoLbsALo ?W`n ̚[S# We,3J 3v8K[M TiA|a.�jH  ob`G!Uq݁6=4 Cx11]mbt+9Mibíb:&O)6N 6Xs^S&T)Y8!FPVIB §YHn1?~Z?jE=VDײvj'ϷGtoC׸%DRCe4ox򶌥#((f:ipJt";uMtZHvDuQv`ΰt9iL$b ;EI @[ֵG@lJe2HqRi{]ڳaqJw1 A{iKQs;k&<<B\ F!/"|4>׼NcK=m5ϛ.Wlc61t<̬&P@ ĕK,+U `fDYLD!E nϊՏZʬ 4&079OXǩG߭qz̽$_]Km\Dw@YkFgP@'@#̗ɎXqMtAH 6^L^hY1; y"r QҥpKQGLb< u_/;^1WXĈtUxjF"SHg$A.4»t7NBo'71B]dJg a&FHqSB |QjjDA  ܞN Ŷ 8qx"#F [ 6K*D 12K\/o[.Ozr-TSob+; 5&foٖ02t,Ke4؆w[r= -- KJ УQSip˳=Wkit=v dŏz=ɳ.V\KWrXDٷ_E73,V8ܪEF\M4n/fFwF8NXղwи b:;0bilDtK@PYk(m4q(tIЀ{D0:d7`Z-Z:1F!}C "OggS3{r8p'T6umMP9ގC~tӠKI |[epJ=V![!}7z@閉련~ƷH)4%4$q6-k=0/ާ"?qWL`,k]:S"A9ۍZk#Ffyq,Љ;H(`_cz40n0iu b3bb( pC h Ab3;톄8 hBq$s+Xe(t&uT>|[g e4gkG6=tKjgeQ[#GY%5!"(Ue΄iTre dQz )|bl[LM/X֥y0~ֹC7RXT\KVdQ%zC v4ԃw^;:t'jpHcBn5s9CE}c_i] O@#5l! h#4qGwmv#$v͈9@IE1BiBhM3B_",A _2 @t8ƺV@)3P: DWu0&I %l(U69C XK㒡3rnةzܔCs4S&CLjPOJ,5UMzL%>Xv^j1^^ NɍO_<wEQȓxWMD0K-uk,FF8*q(AuN9= Ba`+*~w{nH#hJjk_@tMɢB\lrkU"7.gƸˮRI 퇃]MBYmcV􈞷m'4IO@ Fgd蘎&E6 t!Ѣ ! @?2B5ch0OggSzԇc0{FD5gYm;[cL\Ṛ|x6A䘕-*yGE/G-c}YxKVG) 6G8 z2wmx֨Y@)71Z;ӾRnā]:BfWT)Kd-D0iZfK( }6cFhOqOK TBc?wyCjyW; C4  D(B~m 'rBc&ʼnhD(?6"Mlihq~ qB6B0ۍт@LDh.sDsmКјB]/+ht1ΑfM@.NK4ePr/^Ov~w""BQhˣ,~B^iɞN~,"3$!E˪*YqL% 3k Z!n46A6>4  h W oaBJ7$cA!A?vgxuHct.+"9=)PBO_ 1Ѣfk3A+ DLqF4bΈLt0ň 01aLtD^%tk!#~ ~%DžlD+FL֍Xoz 0u16I2R$ |Ch/8VxO_;1}파k}Os Ϫ],2dOۥR0 O^GV_J/B5W\/]`錒]yI/[xq;/΋ 8oVz[,/˵@톫}ni&V5XMX;ۊS7in۝Qn+3On۝t~_Bbo}njQ/?C v_>5BSq_Wi4gWg h\yh}hng35 8؉V;w K/tGX@6c4Ǜz5Եu p[G5@#稙7Gy8̟7qmƎhu <5On96 obyz_ԅe/ p%߱IJPhր./>2y} H²^ߗ\KZcyʵW; zkXVVޑviMPhyT <ؐcYcIXˋ H6#}:v uNX:t# :p/sscW޾}KJRCĒk_5&L{~$A^Pb~No9$jh k(,X>5GBSz/4p\~879FN s.ht͹<(`}=T(fk]) `M0+)\=z{e{Zhbnmm6u]룦Auݠh0G b, o,e]eY^ᾰa]K 7s`ox`ײBz/Kʵī re( ^\쳓 ~{e +ArYެdAA9 {f%WX:k,;1 kkY/k4T4ze-qv~OA@\++wNOi$ ,~Ww ds3I { `gvl)V\0ē$l]]D=Di7Z`~Z9c *sH2^5p4_36}M\iLьyQgD105Wk0f4J+<sR8 k^Wh}KSϞֺښ haZQC봨#˺.^XfY}_VVq`^XJ읜yWV;ZXY/|/k,˲,o;ejyCի+^uku-/X:Yk-4s'NoͲR{-e aǺd{8hfJzy]^`}=qШ#|@8+3`^!YGV,oY>eeYb7ȝB/"pɥ+P ;39}FCIEK,v ǡI@iJܔ^v3^>5m00S@4|ש}}^Ms7}e,0pw( py<Vfo~j{]nkCM}\9F3_M 㣦F}mfۤ.asmRnnn2Rm9P7M|H@͟6]}Fҝ3.zJ@.6B-w.F.{Y{]ﳁ}}&lb};.ޗ/.T@^ޏXuY;{ue]Ke˺ܗj~ x93oޝ 3rݼϜ;Jew'? =zXo],p7^"/{=̱ɋأg$},'+ focustimerhq-FocusTimer-8581be2/data/sounds/meson.build000066400000000000000000000003121520625676500232410ustar00rootroot00000000000000sounds = [ 'bell.ogg', 'brown-noise.ogg', 'clock.ogg', 'loud-bell.ogg', 'metronome.ogg', ] foreach s:sounds install_data( s, install_dir: package_datadir / 'sounds', ) endforeach focustimerhq-FocusTimer-8581be2/data/sounds/metronome.ogg000066400000000000000000005420111520625676500236110ustar00rootroot00000000000000OggSL2rHvorbisDOggSLxASqvorbis4Xiph.Org libVorbis I 20200704 (Reducing Environment)CCOMMENTS=https://freesound.org/s/49115/ -- License: Attribution 4.0TITLE=Wittner 90 BPM%ARTIST=digifishmusic of freesound.org@comment=https://freesound.org/people/digifishmusic/sounds/49115/vorbis+BCV1L ŀАU`$)fI)(yHI)0c1c1c 4d( Ij9g'r9iN8 Q9 &cnkn)% Y@H!RH!b!b!r!r * 2 L2餓N:騣:(B -JL1Vc]|s9s9s BCV BdB!R)r 2ȀАU GI˱$O,Q53ESTMUUUUu]Wvevuv}Y[}Y[؅]aaaa}}} 4d #9)"9d ")Ifjihm˲,˲ iiiiiiifYeYeYeYeYeYeYeYeYeYeYeYeY@h*@@qq$ER$r, Y@R,r4Gs4s6pR4XhJ 01Ƙr9s9H) tNJ)=Bz!B))C(!R뱆N:k!Zj2(R=PRj){K%ZkK*)z9RL-``'EcbC BH)RJ)c1c1c1c1 V+j'tfdȥTD#5b%ء`!+2Q5^+bj, AAe($)XSȔRY%tL)F)BƔc)tZ=TJ @P` CpK(0(I @"3D"b1HL`q!246..tqׁ P@N7<':xH6hf8:<>@BDFHJLNPRT> "9@@OggSFL|1%Q~{C)(D>~mbFCjݨfpbc8_wCLhOv._Zsܬ8u&VV[M~c^i9z5O^Npڕ~6(V$kkzk?^ ep[8n4yr/h}6H(?꦳ ~3=rq,8$` R?57ua<$O|\jۇPy=on'Y\Jq!X2Q50˥MIe>_g1{,駥Kg\́&}EC* "y_(J:t>\}޹c QYJW>^<=?k@{ԑa3{k4um?J":~|&xVG }J=<ɤrgҙo%4},ita7*IahT9; 6?-)' s#rllZݙɢPE™~5&(JŧNQ05>>ff爱A7`ꮢ+\,ac|2 |b M5dGǿg탍k ͻ搛p#\S~uOn^ S_'fgP6ZLsl|$A.bS<&]NDž8H mڨ"8mg9ʥwqTD^ ~wy:)&X?(uՏla'(s2׌ZX՞=7 "~wvrȹ^eaT&2L1 9R67udou/͋*'hE5W]Uv9v O"7@6WٙiY UM:OyEPv&qIJo2Z8/GS '@e*/5|_"j>Ǜm ,P<\B LSO=WE/ 3 zaFQq)g89Q4CwA|>z|.Eæl&weK-:}?~UL;Cq-hҕ{|{=rE?Es=sy{\2V}C[/Pw|eb/sQꫳXw̔^;rΦ)go[Sh^!>a4m;Dhqȶ}ěa$׳S> k5|9v$pFk)Yix,Mu~5|]A  eَ`(@^a0Oql̒I&pP0fGk7mCf͠7ծT^{d\lPvn>znO3)._l.klz\,{.ٖW՚<7&6*ccOD/<ϳftW?dsɷF@3c{29~;oRug|;o}Mj,ox|+ŭivV%Ր<>ůhk`ຏ> P@y|>5|ίDP Wl?FS]%IRI$@zv[0o1ĸ>6>!q7s߶ͷ XsiUVe/uϩ 7NeAG"=a>~CRᴐ!1ž9|[9P?|~! cȽ_`<[nz`~^(5Ŗt\ N꼟P'%b< o텒{T+w~sݿ<~_ ~5:wTő$I"O8ßzroOs?usw4<ߞfz?=`4P'ǗPK(ܧxz| oO}(⿕Vrכ'[V.a!ޓ Y6/ģfeNmJ>3~=L}$sI@35̘CSQ'0c)MiZwe: "$ .,1YwMLL|8cYgGFaa`(na< `pdb `AM2KѫCAOi` ]WstUVGYwFjy'Px7Re_hJ|£6),~_2H$ׄ@Nr`'٩ˤ.ۘsbrwE[K%vƔEgvidFC iqQ!DBqq11:bcccG;{{bq?a"ðF :uAA"v`]PïEeT2"XȚd[AfQ\$2a-vctt/|s2@x=^^+&tFtu<8`q=^Og\5z^~8D4Ɗ4Yt9qdL&{̒wI&22ڴ5 vg<ם6W3bwBV,#e*?rA^[PRI"7i3֤R щq6LPX+kbZ4*d9 9>}TpeXid2J l\ˊ9(!!`x7+=j:Hc>c4 ǵ A9rBԹEs3>'>bkP=CHx :MS}$Nyuuse1b,Z@Ax^7K&;ܐ&+c蟲2-rÌ&2J4w,m ^6F1CO#э{a w45Bl|kw @&- OXt|n$Ewt-J㫭KԾǡ0P;w)\_&c-'\VDih[֖a2 2 Yz 3UV?R,E#o~N8MOB.ИDŽ &2,P=aª'P >S$0A0C?Dw#@=<5^adW.2msuzk dH-Ã@?6X04=6$tFâa"vXZ"w4c#41`_xh`0ŹITm= ]tgi~vHΧt#%Y^ga5u|L_5(xGuϾ@2M@PbSFY0|$j=uT{Ͻ_aաiu2璷Kgydqrp#4tfc^Щ>^_W0Y9 O j;Fd $δ3Q ߆w:$p>SáihSAt!чx +pe8: #)/h3_.OggSnLDm~5TA/eqtK6u B>6lE|#~>'=f5N^ \O(K"X@m}Mq!|9:V~{q͘[IΤ\ WR4fdZ#QF%J`(@=lj֫;MK6TuȇC9M,|{Z+7{CHK 3ԟ\/}0>LַFMqإZ>t:A?]C/Яsʼ)H9)첒eN.SZz[1$I~/pղQMǘZpʧ<< 4lM. ߟ \^5Dx]ҧ]DDԐ.1^ NoS͡'3 X=g2 , 2r r%:/A1\dcqJMLi<ϗ.-'ʡ9~`$z@{zG&a ^gldY7m`([KIO>wں1v{Yo>Vef>wg55YUJjH2-l"S; z'a6wo<::Æ4R&'66?gq v(Ρ*TK_g=˭va&0;@:v )M +>@ _SSwCh&05$`dzͰQȦ6@$603\''r\X F άH Msi*AOo}09e[Z*ʛtZ$^5t '}8\NT KFKH+ >OiԤt10/`PKY,HUIPO Ɋ*& k8z`D\48m"fmfvJkKgl'4L _5>{UJqľqPJB861qϜC$k|*>7М+M-滃՗",?<9kNfH- @͔RHH66;;X^5ɛ\\ ˒ӘLp^Hxf8>w Sm. 'HI xv,NǗ3:XhYp/{ 8~Koh lL'XA&_*@z}_QJm0<\$MeYK3nZ^04mQʦAXEF7`fYntDv T̵LFvv64鋶9O7GC_õ?}"yFE)a!^?(ؒضǵM)SU|§率*$<& 1rhI4ECPM5}o7sl>yu쪊~5󝩙)ۈq;J|x- ~Np-eIf0lTGҿ}jNQΉ۶AKgQr}nrk8)BN6\˘,[޳Zuֱ>^'JEOggSL̶"_E1;?@gd^BVWq5<N e{st%#6qj/e+am'|@Agzr\K$Z(Tamkh*/:ZŇ~^aG+_cgs >nLPW 겹|bI&:.6D^Ԉ͒uK #%?푩׊vG.Ú1<`X'csaot>u &K k&KŞs$Dv@"+W0"!]ik~T~WL<9 -SBtEOrL 0CpяBx@_ez4͈]k>髲Les+,rܛ Dg !Y2r:xPsR8x{1{90~F41kx.߄>Y\#E pFq1V5vpcTxs+P,H01c箖bME̿E[F>._&2Ҫ8=ɭLgvٜyMbn&;3nCozI1lfbs"4{D,Ӓģ_n:f R$~ }u@tro3^kʷ̡Q}& * vEm] T~5jJwX10յʥ9!f4&{ܟ~3@♨|Ek17y3ߞ xR _y諕% rk~=&3m;Ͻ_]sUDzn3?coORdsBnȷ[*l;"0zs}hbYw?֖?ȇ(/Dﹳ2+,f0at  "2,٤- )ko.Dbg2PAtUlN~|!p|Rd[7Mkk eC|j6f]Nj] 5|ίIxB)h x6"5Ө^V/o݀yZNlxψD>y^|Nވy7}5;ZY/:ަo6@XDA֐x 5}8cZzuM]7`ydž>hkSd|W6%?UO\\BeX\'.S4cҊ!/$-yM,G H5?#.jm#sfr!uEUlZG鲸c%d:ޚ֡b==Ix:3dy 1BnCoN*ޗw߷"7"[_qm~kˋn[f8[ '>>zTP5'! rYʹV~5_T WÏ z76,GÂuWkj Xcl|m?AN /0e$aG m5lڰz=D|<Y/^Jl^"l=}X'S Ag3:/YR${Nx~-=\4sI0 t<_0Kox*Wjyz,_SKޯP[I^ =P\W W~5|]/Mkcs6 H!|?[aHDvG}o7?? ESG챘f?{s4{x+OO?rX=oKu<ϓ(>X/~Wwc ur~~K}}v}c딎뱨"<}wr&d2~h;c2ǐ|pN qq[-[Ѻr=c WzJ׃L&"/ p9 .:Hb+ }:ê \ 8`VJ!a:o}r0gH RRetrnjX`]nAD?@!b(^Ue"as>}x̓!*-PtPFE!#tp\$2c* y&F 2ǩfNZf LPj8FV f,*&p]p1ǐQRZUP]EE(؏ IЅcC=5;bbR>]7XKS$@NFl 8udGVBZPR-%|MU<-P7$ߑH`oΰZ;̽SXTuc FdH އ{\eyvų/ML"Z&aA?HnB{O#@^C0Ia [ep9 %tx zH: 5.>Ӈ  ߄zpTZw?U5mk7{kjԝ2-k^6T FsQِ&-Fiafb=G6mڞ=;2ހ? &79T3ෛvnW2l͙r=oʨhKy7iQCY ӚA/K$aߒEY%x̭R`eQ*QS/7N-t:Vӡjhw Lvt`՛\L*9&!"2z\qW$FaҥM`Ʉ eаr= ӡqHn 0jh$B= Z5P\k 'f.!1u..}r! =U)6} 4^ kQk^P@ "{7HGӗHF_ W 6.ʑc_jMV="ug)FZWh׫ZEYݖgk[RYVm5J; )GuڔmXVL{GF@{!DߏX 3RH8܌vVz}82к N_t-[ʓd[/U1g})d~(ޔXf o. %^5T}mV3L #E`̫0Ff~v?E߿7 .z67Y, ᳣_|-Eﻈ}B_t'FVy5lrsBUl"fri}R? y Mh=:̓$i_7hD`ud0za K>`~s~kHl@ʷhXOm55>M ٽJX!ȫ be}wX)?"Y-..#1 mV?,j} @0ؽ&{yw^k>q9YRn)-%jcOV t V= YZsvs5P}t ?6℺Whߘ^;|.A'+sH}kНo8Bs\}S^SddrpEu;'Ar0.S$Cly&CF Mb.|6gDVSw#ăͯ|Ԥ[:uA#gmHA@X0m9Ȫa !@DSFNj/#iyQa)araqHxͱFLViArNZhM[l[C2u 艋i϶k#U.޾s92*ݝ9_9d=zn^JH@΄|Mx9QlY$OTqk!2!= O6w?A]'ԿQpn4M>+ 4P"$?v=uSrchū7 P4=_]peo4 L"$m`Ebha+a< r4 'N^0t8*,^~Yl0>ˮ3h9[G=Di8Pkp{D3fcL"iIϸ'w;x5fyh<~ZZZOi愣;պi'op (P´OeE*j~@'Xgh֋-# ,{υB&@ϕZoƽ;o2'u` K ۿvwZV>aOAy@qRI?O>ֹ@ dvԉnƜI B2? L&Ȧٜ=rQc ]ǗAXi|^ʈNv }Q}qbz1=i]5P,|`5` 3" qZy@ҌYܩ򹘁\ݽ9xxim.ښgFU Ee7YuMHl7rRc;@3ۯ^4 H#jp>NР͂ZZ1$QJJ3^e' }Nq U6$T^Y}r[TFڪa3\ΝcK;iSso_Ξ4M  Yn!^)+wqvKe\sfzV4y:ggyؿv x/jzW$͒M[I9?[l!"> ~} Ǭ8=$IcrGR>蓢1tjOi0M8bX,@AARL*?4`q$[yF'm%M EԀR1"  L{,^DUkJNsaՎ=|2 2ͧmɝܿ@ܡ)e5G^cY{mD۾>6G?I}RY 7J:^ K3g|ò,[I-u0Ib.5bDx-W>z$}nT7WfTWH].c( u^ɸ}fcf(v;D=-<% z6̖k'?e[ 嗑|QOOq>m[d r]8{3;܉"6|,?j3;>cgTNuvH^-Tk+X؎JjwC 7eZll86fkKUr) w(~5\oYcravz938=&P`Q,=\X(`ZP`v4r]pbfWs\륣i ]BAԷA ƼcǒImͧ*{`Id7 `aKZ~N@tA5eNru8aVV; [.%~p̒ڞ}vyoؒE}^Wv[9~&醘>yu .lhׇ0;]t&̾p79+hj<t$;ê1u#xVy /nTVL{` ѤTg]fjjoT1b}#O|m˯JA/le>pUOggS)LM #{fJ J?Aei^ec_MZm~5\.0։+u_~QKc&PPp}ܫ:(,>@\WK,D HpSW9a[4^[_z1P޸o6V뒫@ W"ݷn6cN\Z"뫗͠:VO[89acmv~ }ySV$ޗE ~}hK}JΝzMD;;_CX'N@sg[O~{՘A^' wgnO(^]P(,~.|8 }+.̂~ܯwhFYde9^ o;Ztzfq{>0{6jQAwW;j,H@*Lp\y6|.u@kn宩wg(OPƧ|ڶ! jSy1/mЙ% ?.VFv[SL6opΨg)'^]~>Df~t``zSyuOqȬ o螱]w~8uah< fS7{/݇eu2ϯH Oǐ~O{φ` ;X3-G.Đ$lN|2OC#bAQtc@>'56b) p^40 ަYI, 9 xWo5܏ ŇjxC,l@8j79/S- ym%ΉZ]e;WB3ZkҸ?vBHQRGFt7xR K|SI/xr.Sg?Gȧ/&s}I0_gSt -q%OWfQsw,24=5Q-؃`,/ϷOϼZ`]N嗢#P:a2rZ"x0ZV%@~S%祑_*סfbNL?jybk5g1*/{zW_ז=W$0-ҷxsðkF,_ڧ{za?c]`l$ߟWʭ(8~5_E>z| hsx1lfOPYiSdĕ/i{ ̆ͬhG I~A_TO4ߓ>?,cݩHήŵVfwͦ\8Ʋ'3ٕoxXL9aBHIwB{r4K58@)Hv(l@/5|]x`~ ^=o$.$"Im3*A=k[vgj*gIՍj'beyW>T/l"#:?7=YRF5ql;.25b|x.k+9}[w=WMݜOQnYovKskǙ5ckQ?He\#Ai-{yxJb2R.cÿaSkgrr. ԝ:39 B^sbR]-U/JjQ*5|܎q<0Tj> 4;WSwqK$cÄݽn;m]߳4qۋЉqchDlp6Ϲzwxk>{.oڛ =x>}Ϸ;5cѽ{y򖲷Y}Ie!Nlc鞻Ggdxrog"gk>N7ow>? iMaoN#CBۖIǐ ?8[oJP[!B{ _r;EQ@) ~5|m(-c ÿs"ӣ$,)ߘ#f@XlC<9?mc{#==߿ =~O{b1N|$~?4JXltaC3|z߾[;/[kVrK((;.X@G̡ ;PyUPJ'n_(]oslrѸyxEL PB qWdW;j::t: CGYut} ȫgD ǀFNER| r$:4O/ft2x `c(8T;D-)* մ,+3 yd. , &.ʔ#03PDEiPADau*L[X|Hwښj2 N=?J#3ٯ5,Mr) dH!NA E(E(JЊől uСC[58jV{+;~Od \O .pr?73 y jFd$@3HĀ@HQ!!P,*ƴp Ñdn=UxZf/s QYUDTeE"DM`bIB.d"..*.*$31 09`lquHeO؆Cd.r/W&ۭPʉӁQRXjMDa*)*tdD%$ M `phu`Cj:bDJ=H/]HI+`\5 %)JN(R 3d!*!('"`"w!P4eu`Z ,bTn2WZ =2 6]#n-!`ӵLe#**PNʊʊ=6h,62p{5[w D(D]QD-VadƩ_z\z\ z E( [LuSʐ:^ s0xWx=^E&s\&WNL&uKzS1PJ&q=>ClnWG\qexW}Sa2j1h aˆIϠYĹ B< 8#1eCP2" e^^yz3ydr\!#C`L. Åp eD7$Nn{$d7$I.{9t""3HFQ3>.22pb@vd4}72e$ dl]= | Ǣ|_3;{ю Bu &w?^oi2`!ĭ+T&\zmKqR1p_0|TC+7Q<&]Zt P H] DU(VdWغCj NDxJdOq+>1af`SljSrF=a10T4NCACR^.v$}ڳDSׅU ZPz Ԭ)4`e^m87G^7DhaNM@l#=9LO]Ny"I;T!Gut`<鮊ۨ8gk骜1]lw@r%XCczd3҄CW]2詍wzawZNrHaN8Oo ݸ:Dtt-Rjzf"B`.\d _ zhww `CklOggSQLA^6$AʠRڒ=(eeFe}-|Ym3P@$^Xv(SJѠ;KUy^?]%JDdvV<> J;ݻӓEFn Rg^svG[0z,6bmZlIlgrLVRk`tVPW\\XǵRb 91i|[Ж[] o1qgg~p2 N1Ӈ `[u!CKt+f4nȣM4. IDb+HaHm-Ja0`Sr}Y+i ‹}>}FnK^5ޞNJIXH%G3Py(F{lǦ]=O8Ự'jJ!h^nzȒ, gk%/a<X{~+ڟF!i.qLܔ2{,L"1l-H ǺffPNbrp?@CǠnÚ+{ٰ.yӇE':?Ai#ie~&`QUlK5C@wM:l>G-:(TP>5-pSHl -]…S|kO4BO .Q@ g_PM[lIdF^c'/"z`Nsu5='jYz^4r:Md ƤZu Hֆ2KS=k kBI#o-$v)M;:σX|A|`yqv-Hi Mx~9}vs_'<7^nc Gff;]x335>pϧ]4xPt]lq1Fo`|U=j12 ?ٝdyz = 2~|m5:^ @e.Jqop;;ǡm56y/OrRQ^5lD3^Tbt \]FoPPR>G4F~eG\y\=@r'` .@3 p;* $16I$sDu}OS^IBwyyDK:M @+nKG [-BC=VA@U%@65כ/fB댈O @׷LbEY0@@ .<>q["|"K j @L/:VHbei:% <М@0! HI Xq򯃌s!대Pi::\M-*Oug)k J>I̡_-oe5UI$P^5dliH[H4_"jGG$ D965P(07:C? 7>czd$V+HML9]0"gob!)o Y%*_ C6>]`׫`L M|i" o2Xצy1P `pau7^@ mCP/% a5#׎H%_̹^L^9`c9oHTo_yޛcrb)6h <ɚt xdS(-X=4%7~^ q ȁ'YsP#2 |r Z~5vm);rژO5RR.eGF|~$@o2z} k=^#$|f 싍P}>xj4oP$bEcifQ?-?K9r0 ߐRӚhVX.9qxlPgCr Vk,zqh(Jx'HSt DihXO߬Z+`Q}JdMYzlėհH1m"$K @B.5M)˴LQaB'm<{f4U3ay|cޏis?>>(ds/Wח\JQB*S/~*(PB4 R>^5y LT-+O.tiHG"p; t|ӆvcƊ$ϑj.MAZ5n WÉƍJf}m m?]lHpfVOE,4&?5v~MTno-Q"MY^l/7qgPO-O.gU'R|vBF 4'[vi "c`d@A+8J:sޟٜ;0_loVřΘYb`KBӘҔǛϔ6+;[&@|"!<7*$_KU'Q;W( m- {T ާ-h;-*qyM\p^5 eW:>yrC** a?KП=^~C@ 5l@ c;㊞Y$̧HҶYK;с~}=wrxM^m-` K/˪"P/jZj,/2{wn#Vi(t_k  fd3_lX$F2 upͰ7Y R Y=D@4C^2ɟ>8d`(a߿'t2{)ys dwe>{70~Dϻ0$ ~Xfs?k$cfK5RώzB?70d⤲m>y%=-rxB)!q+y1P5l띫Y= sd3cBre8,ٳAހ?-@ @ @gcASFW3( )H ch-'I$֥YW)bgjyk{';h.gap/ۍ$a: ҏ2S.HE%<3溜 zٓb3,>3-hnr$CP>}3? 7 0F,%]e}ͫZ>Rj_װ!SQa]X;yLkNg}՚y`s 6k/\E;>ՏhƉ,j<{D_Icr{I[o l}?,HWZ&UnRx4Ʒڲ5{m= 8k ~5 sڧ2\ 03N4nP\$g2ym{# $7*xn몤I +odd}XڇV{ţR/3esD-`')Z;>*SZps; ݗhupOյרJ 3pu}/.cM\&ޟ&[`NJzs:Nڗe~&d{KkK 37?>gs%"лwk"3p\:,f~b/j }ZTTM MG0hDmpqNCx5}~ h@s; GN.I,ID?d4 ԃzx3g~GWd*h<iTS*!bWo깘& ?^K2 \e;uu6}Ϗ}w>ӌ,q \w_+i Q6f.,ZKDXj۝N?'ȝ~{!ΡJ7ܯ ퟰ]J9qb$2!W.J.OAYûw~Lx%64xm&T{Ct6>*57D D\|s]/۠sB7Q5fk5˦aJ4zR>۽Vh-]:_/Y) lxOgiiލFeSgiO?Ɨefos>D~z\K~jiىn ZcпṦ/=3˩mG]OmMOkdn@a1XZfgӮU<yML3kV,!yl[?}lޮys(>5|/(`Wïs60|}s'{ P8\lc3 {9N@ضɻ0]}^~;%nUN%$ݑS)`Oovya7̘~9&hNvK|2ퟦAh.bI3{eW4Nz6Мۇ j⧍3s#22T4ewRxSIRloW_okfMg )+m_P|5|/S P]UV %ۍ[>62|/ꟻմiM9(Stx- @?;pCstnK\ ~^Ήs)ߥOfll-;/]mqb8HECFFo.yCRܻrV֮pKQx |g̝qR{~7$ЮkS?p +/X{{g}׭{N}Ri_n śV\'_࿽=n V5\ܻ"blbUŽ)/+Ӷmť1sKF0(qQ!-4 I\u>}z]0u1,6v66bX,KXtGrE~\.W~ 8:\] 8qGrqqx<Ǒ@2,a K r\r\&dXW$ 8qffff0&&&&p\.W&&&&&"s]뚙z^$◀ l!bIXg/VFo&uD^4M,EEąB!q ,.) 'Mu &XP``mwG /§ ؞ l2*DQQ=C$K)QD0Lq܁XmmMΑ bqukT 'fP }epa6 ϲ!ʢ:j%uNg؃$* BJ)2 -P"񂂈( % 8M1zD+6&ܝsZ 6ZȬ *2Td 5PtY$j:`A ȳ̢bY(&(JLnc2L&KMrׇ1DŽ \ޡC eĄ x=q=^dr1z.B\#N~qWxW&dC.^^G0}$ NJ F/!Dr$Ƣz=u:} 1zo0 8&s9-\\u~2>8Y>]5JD?p[}r5ZY2dL@Y@VF*;tG Վ^q4!HE0̈B^ԃaP)Ut=f.y3Snf 0@o{ -äBjvM5]ΰ sdr=:j+Jj:mNNE]U\  =\1%\EmۋߟE%}]{)bxٌk!~?o`Ҥ‹aṻ AuPШ;X@P+wxIc8.%Lӳi=5jjVh%Rm`2lc.`ZXmbNDS!z>BFB@${]0aa( q9 0>ڇTdчw)|_RvD;%t,U!c%H'!`Y_TW=Ksb͵K|˅u؜_`M_~;t0.Q'F @$΍V;쾟. i04t7w)ڒKJI䦋T_܈rJRufhؤa(rt#]Qg_=0TJpeL SBِU6P\J{eY l$z$>5 +X7 Ob ).3zx=jF^kݡa_k@ 8z`r$c$D|5: #SQ\[2}y+'eXjLAo_daה?fQ&H_HUK*@(ZF' V2LϮX&>޺aԑAc>=h'9h3'΄  H;kg.4*9XM#\(ⶒΟQ%J']xGq 7ѹSA\ n |~=V+zxwx㤇;}Z"-Zf4qF Ǚ <8`_c͖7٥n+9 uvsBT$*5D_F9z a/?vǾ<뭃4+2@w 7 @gP5lh0',$};ϻ:i9l]5b/Ow?Ux.r# ɗW.(٦+ddQ'A4c.'*8Ε " .EvCS$@72`H4lMk#R}_y.r ( ;6DKa  =4 7oBTԔkt|bOIOLN-|_[6Р&U%4}.LXB)\iw jU^ uHo9[fH5I/ ߮daZ<;GFBu3>P=>h ŢApZ]: pjp/6z5‰’H S?zTڑYM \Y(U|\~z]iTaQ#ةvHBB6֨H  d^;mn{@5CƲ5I‡VFgyI%.4s+o[P;~6LYj4%,I *9uP; 67weIe[oJS5!畯- 99Z7܀a8[(%x%3lvcQ!K~0R;@6P*luݾAK)G8ea I5?|Psj*l Lg+(dE{%mKAr,)4o=?=Ckp4${W(^FS,3,-2hn>I(qAHW9? 2]Р `7w!yqY_O9? 7U,:Hś!0}}=;'@7ϱ!eW'4rRћ`6P<gV !6&A`#w%Д@Q9ZSWe@+tK9)؍ hd kQH_@,E>[/2OE"%xG%Ф [ϸ ~wsÍjWoud}܋и̛ڣ/53hxDNJySP|`F0fv]EI鐒"_/5tkkcGJhW[@ET>BD@2XœQCɨ!I.@Ȩ3t{n edD=34W>=ċP%]r %Ý8cPmQAlzTnI@DJ_ڲgޫ9a30ɇKg,_0~'34=W_u%֨63`F]~%$tG9<9Z#YxJ[S˪.okRggdF]9P !~`+/'GJ~5p§q5Nx)|`kiŕxl’ T'm̫n?$7]܂hDQn]TDC7F `mi UMD|C EiZ,A?Ԛa oY_a~ fժ@fmv@3n@nd+u%VAu{k}rFCt1 ۆ5Wa'>2dblSA}Տ]6/}.HBAmi;)o(,*4w6Ҟ$i6lf 4U haF<:yNƫ"ad'UpVk,U\~520 z8 wJ\{.z?W: МS8n[P`WK"W\0c!rgb,y7ຨQDm5P}ɵ si MSy0mj*a ;Xo` W0x(a~f%I2 ;>g/~G<3+XLtIĕЪKdꖴmx4i,ίQrf!i, })8mۛd?/F2:>|=38L|>\w{!5aHLvg@[^"CL]}%kP/-Wk/` y݉8I. |+W KdOggSL Zo"yb;Y8CAc^ag_eS~5\wӝ07 Cۜ Izi<<=`yp \Uc'aI @ B;:m+W]󂝭6hk B^@s0k.!s4AաY3 u3Y*zoNPT#◰f[^;6|yӱ h}>v`z~ye<2 |ͥuȡ)>dǞBu̙țOw.{{j<0J |^yB6ĝhbd9}`䯥i4{zM 拷ncDz iC;iO Y~O{o~//?u~$pONoCJb][Za(e5//ā,%Ljė9h+m[몿F.=`Lm1 X$ RD^zKjXj'JpH29cʖ-6Qa0FJ8m=KL/>~oUtϻ}i<1sOw}9aS^6sv4~ːږ>^'9d!Vo'{ғH)pnk| 1,1OĎ `ƞ=9;RE~nAw#?'3g0tOgc#UICؓ$UƓYlI4dr2?e.Ɖog0&[QHuҪ7 ]% $]W~pb%9t>O :'n}Oېag]y9ud7XwDw$t8Ǚ,-qbZe'vSG.Λs'F9."r6k'NM&śiFrE^3 *\=WHGE2~5orTd>;BPdl.;KCr pG 5E#2Um|q:~|ĕυJa"TOB3&Id`[ Yp=wd|y_3]FuȜ}w/K?Lm A[ ᳌ZS,9tP{N-闛2^S ޺(4}(bb[[V}_\kk\S腡G/b#\;}/*jM4{.F?hUk8s5}&Y#"gc0]l>.R)*FLWZUxsz=}f'W\=Y-׹(~5]OBH/49jxoT o}ӹi4J} &n@m:.U$`MU- ITP]l ~,_ lvyȁt9@?` νoIg?nf,.cQaaIW9Etiw)mǓmsu&GiI7q q> Z5b_h~BaǼc7}o&\51%!d^JKo]?8^m49CB@' -O>ϙS=9G׸dz=o}Cd~(&O)&[vz$Fa4 !ٴ v똫UNZ7dA<6zx^x\R ~5|^(`WN.06w'vU]I8tIaݿϙ0ȭ_x뷁fFѳެXVde1=?ڢ'Bf}wҶ7$`Xi( 1>>ҀoNjKH>xM10_Y<*Y|ȝsMWowS RO.5|܎HjѾ >,^.#dD,v4~ a۟wgMf 4w8bZ/Sq%uU]qZ岾c0k,~>z|.:co\ҊN1W ò>KW uGL`>ւ&>,9|1צqto'9Cmogi=to?c v~!B3if|3b. h:9?,C6w</0ߥ{q:?9pZ\tO[Ky]<ȁ96@oO W +/ooO)..@.~5_cTVxzzFbx<_ZaycX'1޸OOyV_TѷZ2 1,}+<%y 娣GQCG";M~.*Q@C6]DZdTS%$4hz@!G8Ag!HUtji˘Utji˘A DhZpu t:etML˛Dw<&/r]qCk">Mˊ(ˌRZ R@Rⴀ8SMG6jд8Q9Αښjq`1mհ:4 ÂC[[C ,~ˢ=roYG3*#"2+"{;HUh |0p!A,(Q h(Ѵ(mgcXLLi@ā0D~}__$%&5ː2RUٖ BX!,X`!!b4 "LD!!NA`NƊiTcw? iT&x(fV0q`lwn.IPV2K֨PT!i"d1""YJ(%"&Xӂi5Z6VDV}1[d~~fQZۭLuK2L,&n*&H $â &B [Q;'Zq{[5L CO9T6 @,e9l,gASYQfF#kе9H {I=DT҃bP#>Ińb4%QpB34LFZWܐ=tRm:ZWΏ:ڧZIdRUHbg9*ݹ$ܔ@, KP [* 7xJD!. i IE=/6VA(1T|3&L&)|DҼT`[s qIH\ai3uu0 \T<na^8N!dqU܌zė#(cr\ɕ+ Mlz^RLu(Ed AV]zG:=0OA!@=1D ~t[>#; Cc`E>cju /\0 } Q|[MOggS3L 2RMSp7Lg0= DtupH6hs麧HP%QȌqD@FJsSM e"~+o1Z&ʹ]Iz#+WQPtMK,([eԍEdaU޺c+*X`xngUg/R`hWRLMHb.P wуPc4L!-N؆G2x-`^SbfҪwi6w` X^7e1đ!2)CzarKi!ǂ8z!kWнN\8 `BbM*>_N⣿vs\i1LQy^CWY"w!6E */|45^Dk S>_igʅrF9cu_ƭ1)_z,*V bAZ-o֥54++("xw` ҉~jeq9D&s6y=FRkIGTIZG ed.$#>bz10} :NZiYl@ w>6Ta&h%c6J5T`6- QOHkhэ֐}eP8|@ŽN2bu; ;Z,*3_맨ǎ85wVI* 5l[7lC2 ֋l L r׉ElU]ml`ĩwoċ,Wξt~u d_ϒnYLg2:m}lnuUdh,8H9Tl0H&-a)^s$Ce}U0.b!FՄa$Ϣ7~g$j^OzC&yIɕx >OZvf7@z3x[5$%[y(84!]3˸4ӊ .icq7!ݿz4n78n vB ݭ \t3V EX׏JWQNݹܨ%-7ü z,o( 8_/$fu!m9›&ͣsw,/_뻬 /+41YHů/ m&}mw䫖"4+_FZ4E!6(6>CƱ&.ѯ bml.K/ l25zFEFO_K* &Gt4_ >=w7tcI'?pH VبMN[;DsWZf-QycIt~ө*U!5-vT (6$H~k?Bj{ɉ&94M%pk9X 4S1KfF]^k%6mkxj46l9YH[M0'l^Ζg)|A'' c-}@<1x ]]P)< \ @mϢ"P3 `&ij@2 _/ K8\;|=Hb<3xB>zKnfלyr[3cp$p]AδXKz|N1 !("' #ggJ"?DlTQ`̡Թr@nYu].ݭ iڰLҡ3Ӌ'w;W54m$/5 )fCE2}2XCUVO9'˖W5pو{^?M8'(hmJm,>6ߵ,'k 453P<&`yFlrZϳ HO> Rj608:@@uM]* mly;I+>#4k½>ƪ~ք {f3:Jn }Q7N4U @`@c1QIǮZ8T%PD@ \ߗ ށҰ>d7 9t43uGq|q3?W8_|g<9^8/ M\ TYa6'ܲR4pO*#q^ b vߚSMx%#>&a~%2RތsbиjޅW5|T`| 5ɇ΅^IXb0CEbBE˱Zj7DIsz9FѭeyGkdIpFPVư{ySDV3'!M3٬)Pj .DRyVW lgr~'{=s]Y넏&jbꞗ7 4?.$q\#?ed^gYܝ"JK4޽AE]`q`#?{^^ܜn&_:׮uTXxE7Qzb.@}p<!9:KXW^K՘pDC۵s?kx|==|_ۦ6E _`a@&[BjvO`z\}쏯O}7hN EW=N3$SF: 9&eYb4IVXDGr >^Wi>b&^xzNWxxw%rRGXg2iy|,>p 'Ɏǻ٪1P- 7^\KߩOdtm-SQ'R?.o]V^5>_ލgTi [B{sR*(-o{tR&^&>DE~ 5LA Y(|50/(  Tx'Ò8~#֯0:y$mq5ǐ7Lᡉsg|"SE⤣SD_e?A{%˰Kl5(;3qԝ5&Xo|e6xF7bfyģMkc 9lӧ|gx!8Ai6懝^ -1 L5!>|X:'U3,밭l ~5,듫B6?j85N06̀<6yTb*cŰƘdbHbs ڵЫ f2`zuv=x >빾\a=+ʻ)6lET&[5Xg/"?ng3g?ͻ|Emݪ9톡3'bflJe%Ah nɆLDud!9B8 [曳nv'#1yp?mhy̜R8nx> t6iv)ILh9`|'5j&?[W!XJ^wߓOOJzj\gk0B[{HtĵC~tM:Lm<~A@g7rr8-:rC wǂ9s%!w5׀5ߑy+,˺|'GCvGd#LbyXo<?kSg.{O|Lnf2u^9ۈg¾bIVe{SC3w8ɲ(Y ƃE$FU>R=_N۱q[]‰2Һ[Hr>~~5_֝xd!#Yri0-zOd`_h?X;,g l,7ZJ8HWz .(%ҏV}!eVz ݩ;9HWnyBh4`e$Ґ$sS|[ݹL#~Iҩ/Tn$9Yz9K4OEJBivEc%eߺ,yMɾ1}rw.ޙi.1LO>߳ϯi|8)Wmb=H uC%;P?+) )k}$>|4v l|gARV)j|rRimFunhe/L&I@ ?=65*x dck5g{u&UU?CVF eo=}jOp LhgvN_<-! c;Zy_jޒgua7巙Kd kh2>^6 6}bΞ7 ei9/lfۚfYDvH8Іs)d b?@Xj2Դ}Nļnd39O>} BO].7W]+xYP=:Z3Kӳk,&=t|@kfdyο/ojʁ>7='CڛMsEovG|2hCGb!̲6vƹ};>{+?eszܡOYDuIl9??)n^^ ݯ߮汭a`d4MwvUd&w>UtAxD"XTAs' b,hY">AI34; {tw/s+Q:?g:l3\::{1U?-qr3ϯ;rl>/!=jQ XX۳t߱?SįcҁNuSoIܭ[gtKs6#ɲ9<|K= DXd=* 92ʖ~5|m@)/Le>@>< 2g^Wg|5.`U^T.7[20RW~h@3S"&ڬ(؜.q`!P^:'NOw3ٱI~<Ѽ>s4ph]3er>ysY;r^9{zs<pj1f2bzi}v(wzԇ94wR?vƶ,\cq?vzU9^K{ͬܢvw.iW^ޒ6LsᜍG)-|7uT{J=^=?;!h^_~&f_4T|R!SjT7׽5|^I2ޫe&@?N4,>@o]9L7I)Ptk;mSd,ORv|?G.=BePlNCs C?'K% $64Pԣlvl*}R^*+*/1=C@S3ݣ[lPGLEL703kd0pwII{Be0}M[6;?,wϦ4;D؎ߞ9O5|֯(/ jQ xmZɞ>{/{; [X O8,$nV@1:2u崋v y[Q}=R뢲ޏ~cYaffΝ}.}b8:a?Yd:aO3>Et?댳MggddvFʴmon uKqOb;~M?[;4s~gci@}6=ҋR_ ]ŀRc~ mq;8MQ|wOPTݯGַVTU ~5oPk$I$q0==܇~4s\aTSO<7}qGOOS+xOO x{{x{x/'P7p*O>޼}/ɁA0/ɁA01CZt`BZ'썿'셿Ga j3.kq˜&uw$ !tvb'Xwޜ!N,$^0vthuA-:&8c@dG؏шmw7Ad~qٻ%*+LRJ , xBʁC'qְ18pb50btMZcWm;a83W51EwWp_e]m~ɖmʲ,˲l{8' ,HJ BqDA (vޡcVCj8ZV5 cM"`>^E|;3,C/+#d 9e 9ʲRXƐ1rNLAL$ RHQ"DTDXEDh1 4!"#ӪakQ1a.zS`A ι)\'`I %ʌeUeED5*#RY(ctt$rt )'\ Ab !H`PTq LV8 jgkgo1 S0Ǖ+L^^hQOSLjId8k<&0xd21XM[t8N'>q9& 10 UvL`  0WJzCIOݮFV$ +#3zbEgCb= '0zjq:^O^:&8A %3F(! dDnDer7uroJH0HծGxڕ+Ḭ0K.(4oT+ANqk5OggSL +MXl|>7TA t\P1 `9"Je]22vv[u%ElW)g$5eº[X#@U~[dδk M^YU} 0tuԶ{^RsC N5@  ธ,=6,.kVS1EF>Fd0-ujşoѻp`- hdeճ8;X1 D@mX҆ɠcڋQ`J& +00Tf~8)`ȼ`  &4|z [4a!4<(EH?YӱM1T,7d0p1J'?x``i#ֱacL~sg1"{u xCIK)$u+ =~Uv?W'06Hg_uft0wн UZQYuVuhO?9$i)9%ڸ=νn}N޸;CtsJ ns`,vV7 :fV- Ʃq΢pծ9=1>Nq0c#aFf~Џ!vGG{1Ccx +02x_"z"ٔ/2"^p6T锱ŝn'F w Г׏tGR=rvO#z=_\#2h{v<}pPT5"G'PzQߏ 1ꝛz>IJ\ӢD0ce$K~տ3 ;2g?uGPj|Z̙d&dQ! g9CSB_ώ6B)J\n ֬x9I|/Lau!=[$q%:{{}ZVkM2P0s,҅E~nČ\ɐ[I<vAxAant#&@o-rV&>Z<54K*@%W$ ~5}d!*#KZ5g=5hblF X֓}|Z1j&xsm]; &v=nj^l'R{ʂo k'u^#n4|Mm6&D="M]h|vHeJV| O~8",Ry-?7ڹ '6d =qwO$Ēd|TZ,UR+;z 49)8([\h[?U$guПMWdVvbP,=t(DnP++>*v1,UcfnNmoE~m7>xPo3],^H'34 sU5no|7ܡ?]CFoOnHi?l}fzhVc,n`2p S3[0~ch M/򳂴b*wCҸJ\t\jls'4~'p1 bgsB6jFԡrMA@9od3d(F\[,  ?R+97~ O@|׼'z@/d`]_bǚ<iQqXA[EB/ Ǫ*Jr- ޛ9}w(%S|66ob r 5mN |,r#}Il-L{My);ۥ8ai ,wVf6̖8/Y< |rR,w@tdqxQ.3 ;v uz<,oBiVSsd D_ϸ ~@7<1%p h|EHdߔ|џbw}Pf ͊ϟY~bͻSWj? F`} l@GӏP,@<&4 вAcPtEx94LcAD!/I_bCcH}@Xt?'K3D2,gdP\|l4A)U/*YQMs-I,VIKx_AJ J~J2W,[Dɨ{l>P| y*h/3 a KJB=bD/s-id_<^5 >>(dD}muءΔK&G*zGTON^ϋ͐ EH Ŗ61h0=Egp)EȷҳlM J'0X@q 7? cU<5 ٠wqs>=];}Iu;g?gN8g!͆{4a 20=1|-߲‰i$s10rEaݷo'kԓ@N`/ ^drWc}k W۵SG[I)jPjXGYQޡʣ^dyߔefN?tc}}X$BH$F ÂA_`@O`G-$ ORґ9/~b4"Yi\% ʅ?maM]|t SeH8r.仴-$80A7n (1w:@T~=>K6H9pnT;}^.W L]B$>Gx#v]E;gRnų8`$:i?h-Wlו0ޗ$j*vkuyy9 iŐ&f;%iG@z2pH#7{ Ј_f%jKNQ Gm@~=L#=L?~4,䄅 lk`}ռ:FciS`jTԲD& `GҸߕ<#8Vo$J(-:eGBr0 lh8w.gIl dvF&O9wh*:U漈y-$.e'Z໶ ,}۰{>b=ԲJBD.OZOV6Vz \2g9F;smU'#c*~+wγsj ˕Ngq4#?<@Y( <:.I_lUYWŪ)~}1vw.af6gy ˎynMX +?0@p=M,,.,ITSx\6mRA~|*de@|hoNƳ0Y{ШPl50$hB [77cm"*QA@b{ e{=Vqػ@gƛ=]D'9]]M$ T?/cij(>lO_J'EW^ui!$*:&&ZLf,;?122o|W[AFvr߆{jqgWNϱUc2̝_TqGI ]L@ڸw^OZg:"x?zW\1͂m6jo 9kx]offp\yzzE3Zs`]!x5 tzrIX$?B$ؗUۖ|e&!jֺʬz%sbĨS4EI~:Ȍ>J, Pﲤ?{l[Ĺf+v:<q&J'ɝ53ư=rZȴ y'+x.qX+XE(ȼ\lw\Hk^EbV~xOş0pt(C}ȌLx_,/3^ruXNh\_Z׭V#h%gYq|ݟ4tDO;4/rvJ U#l]>Ym||y|Hd>y5Fy8z5/NaK=Q]%q{RpZ!ͥ&s7fM۟+y$X-sE}lf@#0+D4=`3}7lG˽&Iɡ<0KwtkꏮEf> 3Ҙjt6v7y2J >BhX SD&OeyǞ "Е?&{8\Y4?l7q4Xv<&~V`v3K9;:c/(b;|28̛$2a[0'~>Pvdzoy;CGU`7/;kQH@N5ߖJxjxL7{m+Â[q;..,"v̌%r'/O?? FvS!:n,b'7DTAfU͵~}.~gUt D[篷¸GmDp"C&}hsKSn&O?۳Pߦt'|k#~JΓ"kc|z|ߙt2kn'Vh- =O[۞(X M8jJ~5_OBh4^ _cPfp[?'1OCgO(n}r2zDc 4PLVר ]F6 <ZosU?Aj4 B&1~s5ѐ5Wz=&&f+7fm봓Q%[cp31QsOW{̓=U͞]7 '˥%ħw0I_Es=e۫T{LL~Kes~1}(ZOKAl25M^9it5fO\2Fx^O/QVG!jbkU`+?,xP-bwӇZ ~5|^`WL7k' 5iU@uw*ݕ)ӖKGkQqB_FR$Imi4Zh-9֥w3{\m#sKO?ݻs䶡wmߋ-ώOmM SG&#g0_P8%$5_?=Z&En/F+W *k)?rr'7=ӓOp3;CΟG\w;"HuhM筫Ԝ`,ά3#{3祉oN'U~ʪN1-/箟;>GbTRH-<P 5|^ABSg6@;@irKM3moLunzͶ[yK5,Fٺo5T])O{^Y1]6?V겸uqwΧ/yw\ϥ?EZs/T)밬:9OJ^g$2#46>}I\,}RDZP)8ObB+.0<3= =C3fSUPB iK[ϛ1|O}sgp6 9jbW8ڥ{)RV}Z{y{{BۿO ~5_PpIOOX,K+x<#;(b;O/XV}O⩺Ox+o'8CY2#`bˆ#D#cӐ .sKCM,`SOPJЎB F#Cg| "GP Q CS9D;PTJEfJ %$dct &,! #u:]ޡSF}t: PO.* Ѕ?E Q*2*, X@fG>P ;@3 G& [M ,jUC#UEmj(rĿӺ޶j,*Օ*M@Hb!)!P⴨8Ql٘Zms1V5lltZY '+\ ?P\ȍbv"2(ʈ.KDV"EE 1-³*|:w`o1AE@&daQoLSꝑL`W*SMD0uc )5 uLu\&5ޡXaJ}~N&ah t:}v#ω00a(<9u 0L]nM^oBq\Ǖx=>1:cQz 8$EJ}梄>D8DI}梤D{2e"#tp%lK :ʎ%w<)X_9+rQ/=X@ݲ:;O~Y+EQ±WGb ߪ*o8EYڻYtmӪjKQPJ"MEŊ@f2S颲JT)`чR~np=ę*s nX'N`:ThCxdH6N,EЈH7>⤆: "R= P#1q=z3YYC>R_ tOggSLԠg>7w 5 ctsPYN<*k8K.ҚgyDu^0db$0{O{L#Zӣ.ޏ 7Of#۟()ԟ?Q\;Ch.K.] +:7XiO\0[/0 d08:Sor͝KWf:sEEx  [ #"0h/ +0Jqoy2ϑX6Ct^o$40LC\ p*Ҩ 3cڄH&@o%N. WȐ @?t)psx= L@HC@MjZ x^5 %y\N[%0fh錎τ؄ WGM۩%Os7zf챠߷7547TH Ǥ o}Ė9.}P[q^(7k9y;s@Q&FI "; C=yF',`Og_ $ C"Y6m |Œ'k˰9^|Y/:RoӇW{tIg6{_M'\џg5K4=Pif鎮31]KYLKեL$p$=ZER&F,o HR>E }BUԇjeE)^s/no@]gf:͞H%%_ S69,>5T-\ʇ]!IܒMHsmjsoz9 4 F#q YE5A &3ĘLN8 ru­ΧY?5ҥWQoעVݰư=t=?QIG[I ߮nP,`$;Q\Wl-@a- Bn<=ٻ/ `nЏ`{~v?3ac*3j].>=^~>@%A,a I{sͪlи0Rlq qfGIƯJx@5}COscWc4 6 uj^[=;pH;$uP`|A2SF7 }kޝ3eqsʗjï~u^5$JbioƦjv iq)a;Jo `_ |Vy,ik/CF +bb2ajy|8@Sk @%@]˓tKF{iO=9ttߎ7TTM-Y&T wSykGH'Pvh1} Gx%Z0 j H@? 5-WB5dpbua oO,n>ٟ?p膄n8{k52o4Y*]8`QCs9.,FH꤁ 8*.SoVfm~DEˆWvr\C%亹ޱ-~ig^ ̓~PϤj;+^5liJl >I6aGIHc'zwGǺ+84|ܠ LjCgE$ik.6 ܳy^ Sιr5 a|/eM#g ! e'F(Pv5D;VHF\P@(4f cOQ#4hM%J4vlp5ƊMvSDD6TǕrP`8v䷲w2w&c9H@Mؼj5mY_ʚs2zؿjY#-,愈5-?UyGl'QHPjsu 14% w^5rmE>0N5tl^,摅:fv7  O;PL4b,$0i?USwW^]*=u?3UQvli @[tݽn?5X7 6MmC_$ܖfc6? U=CKE#RD<uTQ?qL4kP$Cr)ڵW7;xCE- ×lv]L4F(߀\$ 80cyUٗg(jU 6+A~pgOpbxU}khj&&k{Ad7)3e/H }_HQ?/y}]BdH(R٭ h /79G֬JyW~54h C"5L㽑NYRB="a¹ݠD?"o!j`0h.7&*H@sԳD&Eڽl:dE<>9~c2^Imr=y3E`?HHGrZ`b~Yh y+@my6Pл xz_so )5n?0dG/k @|f B0 İǐBR>èr"{:N/hS^j4s[M金 Fr%y>9Mǔ`M%w߸+$Ƽ<}d}wVN80JP}h/_wyOj/m!w>TO~5= Id|Kqʥ5s^$.6=O3| &`ҺjCյs,i*rTlf5QV,︽PU\P5cOC`hL&ΒФ yQ+7hg-h'Z"85k3C Wƕ1vQӝP?Qwo \w1E(Dټj_8oJ拯|YrSs-5kVy?-\~{_p9S/8,%ˆd3ǃ1v'P6^ `gn~*!S|: 3 0gӇT:?M|(ȻJ|+61Hqy~5,󛫓>݁aj7'}>Ș;k&YќkͱѨ7>Mns%枤Qw}[kOr"\7BdR.kJٿv.$D6 Ըm@D|YG5Tl.(4z }$Oj⫽:[YB% ~jen;{&c:rv aDJsʶΒD]ߛWeg‡!gSO|mח|tD?PmלȾej% 6Q*rGj~'n8̑g~Xox'Y1Y3$̑M"8!8h2l U,T=YIHTOggS^Ls˪ iK5?++W}댗VJ|i4ü6hPsv$tw8llx3C"W\cS 7bb0`.͟BX<]kT 6P{[@+Wk&[U8w 0t&Py sG:,D7ͻ(4*42hBm@@P{SgO3$%g  t(? oKg1OSj)+g^Xi>+otaVŻ G'v?P%mٜw$Y'A{3чeZmvw{kj,2 5L׀ l>G vKIk럳vʬvU^nX2:pK5NeYehq@ J#d=wpC).(\L-3zDV73;4uCڪ4̺? S&O.Ag9I K$5ɔ$Ȩy.sMQQes3&F$>; <jOf>Yp2}o5NOA+̕N 0íɞ_Ll ;`Gi-j3uPu~5ON4W ˥QFk.qs&^;ph<*q$ ?$5 )aǞp[Mn#ߠgeԨz-˲M X}.ˎBtkF.[޳ ek/TvT^"bÿx49.hƵϼ{߳;hE )yevaׁ2ٗ f]';sXZ/zԴP:Hf]|UVwR| 5_R>l1[5nD)hq^,p_V&}ꟑl}.r$l"<`gRcTy~?+0t lWARTEf=uZ&):RE{1}//ӱVwO;g"a%>cl+ݓ| sKWNww8p/cVOt圴ȉh#Ų}ܿhb&|Gn.|-s+ݿ˛+Ňm9n4G.?]T_Uw}hrc)M.O=v^_(<$Y{Ÿ0bsY[>sV[r6_{-{j)K v*5|\/0兤= ,sޡKMLN:}WI$Ww;`8eˌ:~+rqnس_f4mXS%em'gBKטC_ KoU~v=_3OxRUO~5|]( \ go\40y>J\QL$Q"Lf)q~EWۡ0` zU c-6u |Y;U |Heƹ }7}Xo[ֺN2=s)Ϛ}XҖ(r'7j#{ByͲi4/ )ذɢ`nqsn& 豿:M0 q~JΕ!J+w>. l}Ϳ󜘱d3AsGAHC/Snk5MOkbՆʞ;9IE=PܢP.5|\Ah/4WL&T$ffI$ ~;:fXͯ>oIcst,A˟Z;*@ouE.FzŬa/Q,v;k;|Y~釙k_z yJm\'1UEٜ0T[(8l#s/\10九J1}!'5Gܧ~R ȥJh~qK@B1?2u|[vP//x {?^5|n7MA踌]$cHXO`Ȍa' r`z/k}u{'C]|>wp~f}} :顿~_ZA)9S}z/O :nwoM@fXlb=}O@o I_ocUWjb/g_tw!܏{@0yHKg`bA'z  13C+4xП5!Vh>? kYtbLJ(JXD :.Bo"D-\ Ɯq+8!P3fK&)=H8Bd!ԄRshAg|(qF@htwm,+coleX̢l7C(C̡8S1#1u@6 C[3L{Ӱp;01,ӡVLXs(6"c,z˂ȉb2EoY09PL7W"Dri# rYG X!@\\iq!*l,b5Xlw\lqçOM}VWXd~f ]o C# 9SUYYQf$3$ݬF,ț@D f&<B`" ӄeJ)3$-d؋aZ,9Bm,zD{5 sn s}$׋c5 $e+YbtGzJ !/,$ B0E10"tCSz9 q`2"L\轓aAxexbuj 'dRLeF^V vzqLcO0 ޡnmL`z-CbucLJ#9XSC9&s\x ª,ă:` w etQ}RzOggSLY)KTa>8YI⿇ IKr{H`:n"Jڹ/`FTb 1fq1"rI4M4VKOMnEDDYY-J-w\".oPb*F?sC7B!DE6k Ug@ǰEgK`%9W 0 o -H Hcd8a9200zcyNPS šh"tݾֿKz.L\)a]hdp0"-t: `BZBÏPBa؆˜/̐: va uUfK\||#R \AzI7$>Ttސd>p!s0d(Ӽ>|87I_秲}2uOKU<$S#72)z`񛈑րU)â$ט]<عX yXwy "QBp{,#G'I1hg/lɢxox? н cGi NB>#rpO )-Z =oϙd$Mm$`yxZPj‘9y=&PE@44}gR߿D&3Y<#Ϝ- w\ef[ί'c"X_íuk#uq}D_h׻;>lӢ^op>ɠt%Lc&S~" D"!T_tRwHz}3pNjY$Ǖ}s0%2Ycg?d-ɗ۩6d ^5TĽ_ Zi3%=t EPr1&c!c{w`d +yyf|5p8WLLvo.L8hۋq%iliJ#_9dN6;͵mc\5BcZV)Ͻo,H@| "./5oui_5~$Zx6V"1c hv.,.Ͻi@,n`~nnX 0PpL$HppjA47'/D۱q GD:M*w2[(j~8P+=~ьC`ԡI :[4˞^i@?$yj$ޝuɕ@9Kz.뾄gXe<*kC|ul7p6,.* d+5y\Z@p;$%z%/ ItRf'W;iސA&AvmQ ,@6}Gn6rH凩V>zsh1m_. >#0rmTSNd e-(6'^@VUȷ,Q?5DS.#Ǽve_I< >r; `n~]?x5;'_ZoK~jOaTt*oc&ƒF/~/o5k(t8m݅z_ٴHx$'kԻ qaRmTқ&@41j0cBкkZV ]ߕg4,Ng|27us_a$3sZo5Fpf!b.HIښڏpI\>cMDc( K-X4l6P8*=pY @^5d㽍K7]C_$C%cA(IAؗՅLxnI,& 8]{ge=Sjr쨟,Z\YI\ҾBj蘨^^8z[ZPd8bMگ&"Gg4&`?j#On  (K3 G(hHqMG/sOT4+EQz dЕ|aX)>_4ē^|YL~56O AC]-mb~#c%^UI(',__ީ=79X=J]S*]{Jb2O'|e{yJ~9eN<Ձ>y^ [~5}X'Rr5lsbr^$gcP7X~ݟuqn,9&ԩHh))WI[GˮUwxlNM $Da!5DGwgPmlj0 >s8 #Yj#.zr aB⛯zp@_FK1MCi+ݪ,Ͽ;V ';gvۣg mu!Ȁ9륉42i.eg7"R"6%g63M>ߓuͰﯕ̱[z[/{2z?-54\4`$j / z:Q7&gá_tmP[@؀@/嶼ȣeݽA[NhΆ0D/䝅5߁5%2'`2qswn(|o I f;Mj7ozy%SXץE$bOgOL/3g0k;#B>%i~=ްt4QΪcdU;&fٲn>/ʲ=bnI0'mޙCaZ9-_!kUv\-l%GߕUl\YrFEé\OggSLˈx!xSW R.Dwr]bI~57knz7\ υ^,<[ ˪W:oj }̙]\X ￝=9'{W^UN|z52jr,/Bvnc$CͩM{iAF@ɆJZ3Ps)ϸýDJ|,D4N;B .K;ko ٬d_2JOj>o´ԉa,'jvFdwM@P!Ή_tUorJg3)%'`)wLF%Nx}n;G5JYxDe!yf~oZ#1hk4ϱo8 X* Rb%g#@UK-9 Jp~5/ߦ/ n%& yDy`]Ͱꮽd2vyj&J K=+R-V%9+aoi1g~6e,Ag2K7gBӆ=Ӻq,6cVٸ&8Y,3ɀ&r)86ʔ':AUU< g ~5&|j޷ϺᓔTzJEǴ1g5@,:{ h&KwWŒze[lUZfJ >!YXP LI,54쏟;XdGmwꁉw}Ξ>%.1\_acd_sp'wX峐`G}\E3i"y6KR4t;uXQ>d}g{L:2`>PRMoߝ[6lNa6ˁ$ }ͽQKBDoHx9M^r1M) D =%Xy?I f}j^;7M<λp 5U"o5߀~ݿ9E} z_ߙhN=qt\9WʵY`XME׵N{OI[A_v_)0 ٵ[ע;E>Hs=q߿ljex< sOWRj[YCdG?f4rgaf{țsm;AC=2<⟗̑OLyf8+j;;w]ד띎s}>Կ6,|y%rV }.]FI?^ݵH٦<'˾G-ʭ׵ee4?_M9H PyXN}nmߧS|9T 2~5_T'E((5, X kl)KE@l?z.:i/nפy;7/_FyO 5C nciQU&z#HM9н\3i7/{ND@j.Îw::,us ş܎K^^?AkO dM<D|u+}|kt3g)AЅV[qz4'b@Z㥝ikֵ91O&Cb,gA(osv:k.dK:ob4X~OrK=wOߙfO%})ǐr:)Ss(߱]s! @jQVC)_Q/ޞJU|~5|\Ϗ@paJh$$I 3<8suU(袞{:}n~64sq@?#~#ҋ9Oj;nw)8Xߞ({(ܧcsgg{7Xbi)Uy{O /q0?B--Q'\@'[V㿟CRBqP[qy'`o(޾7<V>_(p]~5_PH[Vo퍧'ooӓP'bcP>=Uoo>^udaRi0} PP/VK)YvD$Eed$$P AB2I@0CY PB B *NhvC [acZ 83@ rKa.6]!ڏhb3]"HaUig\1G=8(MKH`A DiP""l q$`qNLlLz(,nH=m2eJʂ̊2#0u;ΎΎqo5xUHC808|# Q1Zbbqeckbgm5L1Od&L&@0E:^>}x=^!Er`0r=^"Id2dç[z|z1db5Tчzv1#u\DŽ1dqtmFW7Ft^OtFr>d2c0 ،aD! u.P̪ǘ+1OggSLxվLo8䠏}E(8h $KLGQRF"k\#)mJb@ nM4z^(Hpܻ^twVo͊^U\LHWr7]/յ/WSbPbcg PM"騛%+qC'^Y$* &i1l`q*LYcQH)FhH \'/&C D? :4$zGdzEk `0GPN:#J<'-ApBOuzFjWu spB1ȵp:`\?827D$=Bm5{._c3,.Uou8?irdi¢0rS^.(J!(m\7_atgyح>uCZd{?qO>l2^㕛WdŻt+X6"V Ud`>39' lKO8.Y 4G X1٘˒VXfld a,n'D3Ixef230,VYAXazqem'c"84 q;z E' !zL,cf[uR7tN . adVi!S={)ec^6TLFs(k.Ŝ$GFIO_SF컉ʙO=rJ}> v KLT!,@R inS‰LhVF7+T(ѺQPvJn3 DI60(cǍFE%ɍ}g$eǁTX/㺼Z=. o_B5_'.OErz&*J#` Zgnc~7/'^^r +&C{*~! zT a "B' B1D]N>8 Jp:%-:M?;ݨbhFGڝ" t:!(UڰC^> K{8JhG 6#>53iEՐ ie/0`<6O4X{vw_@Fs؁%{mιQXq%u/rbCmos*9tj(G3q<}*x؀`۾:?C\|CU$mZ)5wHݙA"l6VvVݰYCN!}׳isV2zioR@z{zvCK=gႽxRkh;Zc ͆=ggYۂ 4"է8.  >5T5OÝNE%YKI]n @$Mpu}];WVe\E @`j@?\LM.f2ړ"JlR/Nˮ2ߑ)YgY<c('UpZkS[{$dYj < Fqs:\ώZ HRݭ'%f"#s1O/ ,9:r‹\39_^㳁Xr!trȌ߰;^ 01f Z0l*pNX' 4Ko8Kw,È`Bk*rIPzɺ,"Ʉ$Ci~@ ҳmExͤs7ITISmH"e5qPXQ6Ƃ+E7^5vҌ2}{leZ`>?m3G2#- .$ g3ɆhpKI$1 +u欒\!bbK1ҹ>4=^s,@WA/L P q=7:D-9y1A{r=0X@> 9RwM=v $kC"ڿeGQ#pגKC0ytXmq |:l݀窟f4N@S{>m$Kg+Բ,h-* |*Fr!=Z $j%}3Vy\r|Gc(`N!X죕 XWq}${ԇ jw9X/x9fw<^5z]Rxa:.Of0POXdq"gт 1ZS&( &*U2ҷ.!B6̬MVFEg;M 5)R?տw&rԛšN&/ 0^ȚK?$Jx4FkmcFٻtlaJqXȅfYf|uW-mZx;hz/da2ƺ8.^dd@qF$@uvol( kk&qX(bC@, >~)Ξ:swg90|k;OJ-dCᥚ* T"yߧ:W˿J^8i~O| 5@Aw@1 ȝsjY (^5v O3NkɤVB="_.Ce\D?Ml47 7zCqM%:3 QJZCjde^\2}Vjx\<>}7s zG$[$hcs7@g! ZO @3fgy{VɃl|=1FUaȌ 7a u,ӍTȗqkc~Gv^\lMX{Â*=賍͈v xjRcԇ; (UY XI-tP,~53^v,DjX7]Z:"vE }fC34{Ðq؁s.8L$h=ĽDk-|t{/d^IXZzڽgٵ^,{@9[j!NʿP8?,b@$L w 2+T80fh f`yJ39Ll֊%M_-i~4mڻ@ďz؂cDz\ذkfqSo*Kyvu~1f>ٙ&{ئXFhݗTk>*c;sC=ߤc'oֲv@Ü`*)%9“Hϱm<>|shqZu-w)| Pp^EL4Y8<]uf^e#Ry"1ƾAhvUŖQ"8gs ܺA$/D컡1O??>Ůf]ohu`>@[X0nY8lлE/\A}5z2da[pЊ+~י̀G_.|Q*>AZOCIjs ~Pvq%6 mNIdsi;{G8>5).-1g~l-|9Ē̂&Azijc2\uXӡtk}p^ @G2| `OggS@DLE$}dT"4@yohK~5;f]u?N0&gDŽ<.MDḐ?pQ:2J&_0|O~Q%*,8} ݷZm *nXc;J%+= [W8=$q dɟc?ן/Mh h uHY M&{2i$sM4Kf@?DMՌ|h^ٲ@ָW7qkYhޟQIvx/>=^P*ꨈ|9[Q>>'&AϥӍ_`3.'O\I޹JdөUZ>>)LK0eCF7caFb屳[̀i#ӓ}]@ضnYXA~ɱZd',w ~5ܯNUjxןF{i#fY$;?s4eYzc$M)q PC= Z3%caxBY!nHgϦ+<*q35P[1ϯDB- [6d9 fܪDghH<n}Ys} e3(sz_uu\;!wK#[*̕ǒy;&OkΏ|ll>C W:y蒒M3ÆyߧϘ's_ `p'Mvpv%+uv{ |$p<-4˻dլ#>\٘jJhIMJ<6,ޮ6tZMi>m6Ι[[+KrzMhmN5ߨN 3ޯ?I}J+rh==p|nyqJ"I>?|'f{!=̊(<8Sάpj$y궆uFs;d"aS9>4˜g|6jAhͥ%T!t*0yAp-O@\:o,K=nO{pO#L}C58ѽa6ZM]K_z#Zʥ>b/?xЬ}M;#svpt5.i?6 .Q~`*4wrik*wŏrlm 4Ӕ->X"^S} JP+T ~5]ORePq_ o\h/4x}Rydg}7iW@wWuqٔqu5tR+i^Z`,@0D=~ڠ*ť_9VGUo1yϫT;ir3ŏ3,ɼeJʥ)z n:Qt\W\Ӕq.x| b<k|[:2U:\'ŏN}q`+'o,Ϝˇ~}zK_]3O[[ S3`DV^rh[Ta8ߑZ+</ҶSZxWq_o{o`S?ZT[ 6ʺ1̒HGPq+~5|^x`(R~ګrݎDHU6?E#?'|B~:=/J@~1m L6:,*7۳y~ܓ(^¶v[9tO1? 3I?P5q0Tp_ ;8@046 YIgb'6m}9F5nb ߸"9ל $6{eͷJK}__۹vY_\/xUqU\Qe<9QX_-&y2& d'ѿO H~^X,'lkNsC7 a)ORN7ǹx߁xrX1p\|+9PڞQB-z/BiC-.)||~5|_7S@= U$%?9$61###Ȉ*f4LO?ߗtܟoO=4s_z}̷ C{#/oҿX C'QqG`ǷO?pxOǭO~SxS#NLDcbqR&s\,.š 3f@~[][/ טNtt ]cZha 5To0 cCw UveVWYifzD8aAJEDeB(+zTZ{3Y.΄B'b ZfiΑaZmV&kt bw:["7lM.ޮYYQeuʌ("=S"t)X bI )qաCujСCv "!ʼn8bQ1B ڨՑc[M "!ZD+OŸm"sn"'bZofHI(*C m^z(=v\LM9$,+,᱄ \~ |(aJԃY ^rW&qv 0@Ŵ3x>>`$׳ qב+b֥OLXSOx=&&:&QAQYEOAL&Lx%;C%zDrB0 *N詏D,F҅"&}Ahq45+q:^zq^rqW&x@wA#OggS@pL#R\[UVg|7K⿏EbycC`W`T*t r./]vEwHׄldPsrb/-K,2ߣX {=(PMF^Tn_xGHE'lVjC5Y4*B3,Ty\I:FوŽ u\oіq:Laxa+1&9yL&K1uNP)%P;F G$Cs"D{# &a ̑ 03 x'6dH؀~{ k@B &j 襄2~6䑎Q.V(mmccvωy [&l=vMPS)$un$p#ʶJ~x*ſcd{J&:}O#s:ʌ4Tnv%ʳ{c9dw*~_z0Kǭc2#T/s@ךp.u DIz\b*|_Fn\m⭚^#j0jk`<yoLX\37hl{9ZZ dПeDXKx5FB%`ܩ$0:Pکڨ:&h7'yz}(?#a.>FhP)Oll5ɒ<~le>.;w?.U.Js4 mertHZC2&Ҝy=r_O!_݆?|_{5J!8H6e"IE_FPOteېv )SP^\ֺ_ E&EET2kq͟MU^́<ח|.uhyz񍔹Q`#5L&- U&r: 瘉ja2daz=׻zg [}]0Z+xɥP-^+-֭TE*ڭz #DD{5T=W>)|"] wK6 ꏿis{F vpCdGd|<dO2JKl50a䰸~!`j0sno~InRL}:las#5P#A`Us 7޻ @M L ,~Mڏ Oh 3L}ߒhp7Fρ;m5B'΄ P9e 3F98,eRssh+ZM_I x[ c A>TDS&L~f9N$6ƾga/2/Zk e9#M]5E)O`9%$(TR:5溏4SwQckHm R|dԇ\96\h@]?Bkb>P @ nG T5f}1[Y}ֆZ "G#<&4ILL9۩fph& 0XKgzQ[HS)U$#st|"8I-IE$\5j8lr IV> _ dv/ynˁ8,$tTC|Ȟ K4Z0,{lQ&e)d_ux'1R&R t Hx٨'"t ,r[:l傐,diβ\ 'A5iiI՛a`h1g \ʯ#{w>Ԏ< 3$mˎpȣAǼ4lzj\^sVPuN9sf^@Pjё)"@ (? `ڢ:sh&@7퐨?sMzvWO;xuεaIu'XjyI7I* wL39uZk8;%b<==b-c+uAm}Xq{4llP^0+ÓW]2Smo\ToM3`妆=?8ܳ9o+˅LXV^5u#23tK/Є:G"݆shOB37y pp4@%$;y$)V3A.>?\Bذ9;;sgMTpMvm`S3 o EP7_@6j?40Y PE:uKhZ "ִeԙHهE^, A$'}A0$7aW{nsAG4|gOf$K31`v3!<;#!f'١7D$.T/-$ {5*Z]NQzy~ÿJ `xxUt7Ç0Q 5Ӏݜg}v|n,(papOԀ$bN:)IZW&A<_wd6}mD׌ۛ [p 4*QH= BMn:f2`M #Amú d@e~oWQƀl~s]`?Vѝܭ|FMbzIƦ}3q|wMhI=Kޫ6 #/luVS֏طEݍfA'<ć+D,".%?7zZ7e/U&u`hutloUr>?/PF"("=94_eR _s[hK ~5lsт2"G,y\ op\ڡ6 Yij`89lT Sq/k@W-c{ă~h gmR_˟biRJdu(UM4F~;oHj=,q񪰂LrVFERJ O_9 OggSL,;]!yUL `$?Kpbdee~5oQ,2]~f49a!mm}lvMfghd,7)@'UBT‘HԬL.`:jG{[Uj7u`0< ;sPٿg>4@t쀰'wI_ʀē"5v}ͽv`B咅ͷ,L=3z]dMrD 'r\'?cAY\H&lE klRY9\?bPa0eM}9fKټݘ>3>{MW~?q%5 3Ĝy>|{ ARBJg,!۬WI|4_'-h 3'[kgKH0,53-;L0y5]Q_O-s_dml FgC1A^o=%>q}e/7̃b>~5e%\Uё@2?VC!,:[~1g"Esb$gJtOcy/~<"cr{|&-d||k{\ O_Afe3ПH?xXMJ{;gbf> t%rϘem >γ_:\>9 1X6 ˻͠AS乨;Y6vv_ѠGy PӏK$sJ+jmUՅ p5ޏߨTT[5\wBP2=Gi}}gY;4z..]s1HDP5l7k =m;{W-l虪~\@@f5G-oT3WXTy%YL7ݯl~s_5[̞7҇מH"~܎?9GJ%v%6+UR;\_{xU*Q2M~\#MK]ܬw\&i׍a }Oc|ʉ`&VI>6DK;kz =/XmqV樓%\A}"Ctx' oI^U?Wnn/p2j5^c*]P.L@VZ*~T~5|\A)/U4,4+LѽOƎN~5놣b.9N13'y+vIGt7Wk ց`|ڥY+&p$!u`;,h@~r I27#;84+U}JuN4y[Cw.yIixbHtٸ1q-w*/(`ԙ^wgάZUs%^d|Lfsn2 che& ̽fvH킬nh%i;u&'&7N:_VfӼIn)Oj@;:g5(8HTF|e@h9lWuVP}GqUm %~~5|^`W~L?cwWRNggilʛlz}2NNlrqZkj*./ν;<#=y҈dϧc|g^9c73q?}x5K@~f eucL%[YosCerRc%+[z{+-kYLPZN9|h\~5vPvɷ+5(-=Usna# 4^SZU rAxhŪ=?V5|]֯DHh*x  x3Tqba$Qd,F©?wOG?zt6G ;_ GcӍC.?"k\7{nX_[{=b|G׾Oki\]L9o\zMI&-2d!0+ʯ=ܿ&z9(cx4f=4M7ߙ~_Ojǣ|0wO7@cPOmsw)(ny\<56{(KJUy*J- KcWm@z+_zto^5_ηDIZ@,YM8LHO,6U)S }zou|{yax3|ȡVXPPjpv?_ [ 5Ό?4sW~}* =L=ߡ*oJy~G%Vo*nUj}\MA[O{=/^{PG8o6V>-rc/QyV<~5_0$}zzvbiooooiP'_x/ $0O'oS(Vꇟqb^1Hh`L4Lȑ#ߜ(3NY'1K6y5f!"µ B&#E:Ah0s7lR˛.hR'XR[Js QG$,!uǂwt@HaLhl Ԗ5, LlXhRjW*"3Vڔ8)zKlDDEh1 hg!egΨLa[ƑE,r:Dt,0$v[a1UL匭*&l2"eee(2 &jr4Tg<DX`Qq) A ""b({{{{SjZ?,F\~ ~_~ ~ۿ,dQc ET2EYQc"̪ W S@J""dI @\L\ A$@R @Z6V;G[[ӑ4j礋 )F`ר- .+DVTYcH͘.1f.  BSBMAi"*$4YLHQ",jQšbX4Z=s;l`]wVO[+8ug۪,TTXeUʰ;$ڈ B.xE HLQZBaqq""eQ"lӁiށE OggSLw;A`H\p46oriHnf [TȂ:7le `ѡWP(-FS8QLD(" BQ"nBDā:հwP1 l:W܀ tm!7[tS Bg5WD܃5;z\t<;aDШ!̳R d 2axš8s@.*er`aΚ*m)!@{uxV]9n+bx=u0:>뷮n7RSE^7+̇:Ǵve NƐL . otbhTGhdSRxFxBt^AoX2ۺFp088q] HfY /y 7DR'7=Zǘ82] "a$iV4]w-HeH"z2^a}I|@ʘT,Pv/]~# yf$d=')cuЋ=)Mdq'V?]jf ."zEvH =D 1ln @:#'Fa}5;zwK#T}vנF41rdjt>Dڸ:}d+&WIPV#P'+L8#<t]zɘ1zu0.jS:+26|Նcw:qƬHD1Fx8$eǏm5^'kP}"M2Tۑ.UwvLKR]^/eI`sJ+iuĴ21: 089 lq0P#\9BU3s0]@4 p8c+@$?la} >F@ͨKkGQפc?('w#!9}ļ/tu7ٕgN@5s)39:P `e.B\6*.E-5ۥ{siYMCmZ0gЃlH@4~@d 8quWW>d2L 5.宑߷g<50~7v $Z~7tX5āxDϺ 6iRh(":79-WfZ҄  7Y9<*SCa 6z=j+bۭFv VD m1^Oq&&xk@ S96*qd1[G9~01pa4l lq@5-C\fO25K.eP {). a zS\Xl.l |F#}6fP r (npQ{.H'Q?Y6jgeU6Æz\̞u.|}“};^ܮ[Mf>k*d]U [`C#/B4>J'h @q4s]?_? ^K8Ma%Sr6Ӑh.ԧ% {M}^?ڰ4+PĻ }, H)H3XR:KH*> Hd[V`⻦ x]\ΏkQJScϐm1ce[Bb(5f%r*L`C J]lc pj^5$r)/', fkuv>4D|.['AꑣXb;n\< qM0D|ɛ3qrQB &<`UWeԕ p}w .{ɂyVy2G؜%Iw .WOQ^'|7*8bb9HT 螖rϗdVBB5~YҧvU36]Cf +a7 yhRl15i޸׸08 gO p; \8e֧lU_woraTO\ɮJ>󴎰~WnMKg??̺X^ė4' Y7 o=.|͟;4Fpze0D*j|𔴪(92 Z~}-1֯>ƮlRF;u2G9+AR;ܺҜg^EAf? d C3r3uz=ᰃ0G)`ȾϛIhz$jT>7F| E`d9DQM%P!"8*+4)mH4l{|&)gž+d8kc<`?5t<ٍƣu ryY.le}XAHwY>^d\h.;Q]dر"=]n, f 44PcA ;p7$PH l$w>&++K۱gAג41b.~1,/\0>M}׏ߚoϲv3zD{d@LkME9+K;%;PiQ|a/Yjηzռ)P\?2c_:9.|5: M-PlHЇ/wZ*4ҕ&[s:;B9y.+.؊e)*Cl:>3QDBcQGSh kۋ]l=~]<_$F%/`c|K|(;V ^5yO#RV [/V3fof7. ' %fK"Iu5T( +lprcp1lbq';j~xA  4u牜3ȘvӌDp3?Z_[3׬6dd?g~ͧypOfv5˂-zKkbʈPs'`Ѐ]W%af ?g^iz[Au;g|>l)(p}펍&W Y'i82m@.ط75͠#؇ '?5x'cIU B-šqE B?CLHh•dU$~(ȫv8ftD>.x{A,rf8Mȡ!p)Q ng+c^I_ Ț을ދ] nGX^!?#aEV8Toΐf06&du fw}Ήc/=M~6d*5 3g my׏7&s&i8oy}z d{rlc$"-2c &fQ%ǀ!헢ib>ADƢ97)j֋{4%vjSj^OW9@E}z&Ke3Y^>JT͆_P~5{T7d$\ /cQ0l|g5Ƹ>u]_L A 'ju%${}D7ÜrWBo[QԮYh=|>wQЮ(fX %Bc竦P6g*~6"<}8:+;;]'?U^g,AFD4CK}gͺ"$ A1IHJ貒hD'-.ɎOJsOߞ]Â?A#+y:iq)?S[ Bj=/ZocL]JK\j<8$CTmHdB>Ä0L"H~#IH]1-?+?l7ol]_$*|گ `~5\列r}CJqr ' $=π3 𡆏McObI"I$_49C5m4O= QykrYyW7 Q5$xAtiP=!q d)3;ل̓@g8Qg>9 `Cd֣[gre% }O*_︿r㽌v^JX"^IZ4;l߿6czhGe_-~t, z>.au^O+'dluk#i< ٹn7ꣀ\*1U!m.Ɔdu̕Noiwn^~D#o>6yc$v~DH?38@g @ 2Q[ռh<97 _X{3]T~j:LML^V|/?+MnVXD Ez:\jQtGzLv|N|O>wYgΪ]r1̅g[,o<}{Cj[3/OOu h3>0;Wr К%<6T3&Fh@rq'K|MaoiE+}m+V女jC9徚u 5PJ{؊Z[}&uz%b4k`v0S}8.'HHHJ~,̺0=ټZRhɰ}6h5vOjx@FeDԥ6y] y;[@Shd窟}_1qyocoD;~Kt`:6~6וWsN346G3g?fʎ /yN}rկ;6K)syIݟ m|̻}oцh$=MxWòH ,Oǖ٬JL<;Syf2b_v=D!#!9' *񡴵f])_ϟ)[5|oJ{!jxLA4}(x^{ifj.Ҥ y>i91@ϑ<俼ݤlfi#rh}5 0S6[c#Ϧ= LI]'>޼K973]df'3}†{q*y)Qm>猱\W_b}h%܍^':/ );OHKy;qct4i+/ ոew{ ,R=#fg̯fs}9JÇ2Uf>݆g5oC+{_~w'ʚ<SM:*A(nX𥐞ދv ~5|^OҠS布p,~6pk)_lOTyD,,;c`jW1O0vsѮ.qt|eqbG64a6Km{~*\3l$rgo OLmSM;N/P.zz)yBostEz9u WrbJe8Ԡ-?u[6p|ֆ=dV436T2UT5ߎ/`Wï84 _TI2D@ m8kߊ8=zmކOOb@ FW($[*}}kXdvrqs(e4uot%gAoˠs|\kl2n&{"_t)/:~Lڱrx_Z\yGVμϕ$MӜ~7 ,YyhFVJ%*~[!O79qNsmsR`Yٿ#P9?wbMNf2~2"5̟G%?]cJ쬙ϱ1w~[* 5|^T|bG6.6UWS$,x1`;o8~54~~:[hX?**jl沙oY[X7N:LW,o%Nd`D#'2N ?N>YgX$F34 8lhP i >r s旷dhW GmPX,)`+J/^ 0r/wϐ3G;gșRffYQ̔^Vme(A01RB!ED"*&dQ1Pȴ81CcTv ' ES\Ȯ[8ѝ]O(BvOVW k,JYpNl$W RK@! EĘI@l0j{#;OggS@RLD4>Lan "#Dy597kc+OДc?&(+2*$m Ʋ(#ʐ;NǡT*R΄[. RQg?*bFpKL)s\!3OX]Xd\a2qg$aqqLX9tz=pHgpb8&q=)|tdcuxc@2x=fNx-B!`qEOi$bz=30:&@71\"(qFM$ё;n}-0B0>7q ǃҔ18q>V{* 2"8,Ed6&œT*J9%BK&6.էsB-q4/Ngh t9U&V0;oW Z̩Vb*n+8] Sq,)n2C8릫B, “RYFAw>„zL!("!ޡ%rOupzxy=n k ErH-wh\3=!̐uFb` !U?^3u~6Tc\9bې0>Z|}LpkNfȆ^@!w;DKݴly"֑KONBKY|n4 Gj?@oEvQ8^nӃ6rG[tJ".sz\=')o/XiݧJIVه(8c|:D OR,ʍ-?*&" Vu&H:PƢ^:IW;8 ̱"e}XOAq0И<=CbNFs5 ` lT܁;4q4@6TٜYnՙ(lkÖ,FGϧ1 rgg>VZ`nmA>0r&Y`, X,-[ȍ}.Fh2WO{XiQ1'j%Kz y?Чr@# SoKmY1څ}ƘE]LqhM:CA0_ ~0n(E1Kuuiwz.e4ӧG%7h',h{7ܖg Hv''<$#3|B-"*΢I}?hZZd 0]KtT~&z% 176L|Lѧ= }K`2zd|ݠqݗ}I/\f=;ݛizbdF((o'E@+ul+J-0i\&j.  _@?._ѹkؐ2a8$1(p Sa">3t6 @f\s{M^騏kzkgȷRܛ~_+n~Cʂ YRV1ge5$kIF 0 90en[hj4'dG6z!-14+>- a! wDI~ j>i0Lercv%ܺ9nQv$l $TzOqk Bt|emC%Bv]Y0@@G B"xqh#\Dž;+,ë$5siSc]aN'&Ua ?,3x+~,RW:08v D$EEV?-Etgآr$zKJ㣇)ǹs |e3&`_t@)^N^va:֩VVĮ*5dzTV[]C_O0POJD938>O 6׳@|Nګui@æ$l6J(mm~m-r 4ʰˢ?-QjwhJHnPw}mlPG lU 5 h @?$ >:iu _0Y͜@$2ߑ_֌ Bb}B ,Zj.֗b62HkQ'N,q_8<H`I+sy4x[[,&+#!) ͷ F>+߫3µ rp)p|W)5F2 }t ]}F-Ƈ6g$p[1G,&ec67\ Y@KYw J\|Xڴf7 |i xx&Yt+`G;F@?5ze p3 5H(@: y.$:HSܫHwڦ6Hj8_ @E羶_px0l/[٠N: ǟcה$pQZ)Ku ¯'Nv5NU Z4o2(8 %G?Avd3)hK``s@RnMDLw{r 3ky65c)Aގ֖<=[OVuS*}|빏7;`?{v:<^j!bGK ۆ~ 8 np@4@Jc& p&*mmh4+ 5a‚.!-%T5;ӵb**^X2kKZ+# @o?YM ~4.IjFryֈDZ<'n~ԦLN3{= 3QnɛL̷uK!$ġг'>DDI :FћN$;dsWd 2c󁌜p˱E^N a1HZ80x49n,kNe=Wmsi!gD+Ę`f4CnɤKd}~+oTiG%k( ~5tW>L5lC8MJp]_&?4c L>:q ԰OL3XӂNRͳɷ7{귥}r{;n}0s45 $? (~ 3_7r+@D6ʭ>Dm M#fҼ ԌGD'n%/Q_~;7VTϽ$P$޼Q]DO+>UɅ\WΛ\aاwAn:^ASs90Ѵ|[j>\̧͗G#d>4iw Ȭcͨy:X$gŶbplWTҿ$ť`CTrOggSLQw[C I+Eab~5L=t7lT\ p|hq$s/Sdsq柤abK 8`tbQe_hP$n$/P mxQ0sbr:y@_h,qVyn5HШ: ~65S;?@" :Y7ՀX:Y=yf[FUZ{dR w\Tgӟ$[.LO'\`i* v3ݲٿh俜¸'ѝ'#2}S )fwGLu8$g/j{dҚ`;7Asirg~;f2o0bV!ha'EjY4?x{hhʣHwKeB?~}'(y=P}*CVSe J_Kя uja2/^ l~/썵15sޟ]%"#g8(2w42&EERD}{8Q^^, =EN_&IvF3'!<ɿyۑݷ3FGį =A R7^=P4\$ `oVAiltr4./̗$ XN.WqP5\ow+Y۾J#`ܧ&PeD7{w`9>0`LO*K$x86> П+&%] os$y=JȜm3T4 &0eQhX 49,Kf]nn苦-T}smn h2D@]Y?ގn(a9/ߣf3?vEkWsSwp"!!.ȏ  \u,i|`+?hNXbN=#zAiL3o'w 'OAA7ʿLPZ`47)Ut{h=*+*0j!(kP_@:i;^-T`/ P ~5OF~ljx.A(F\M' X^?+qM^&+"sL$ߐ^>/O5 =c~8~ _C#p5ZO>LMh~HgwHl*^+2cvMv3 ,f`>.6?N.׎3v]KK|Vsُhʌ<-w9z'mWq=b K^J Dg?=mo`3,zwS毳89+x=w ׭g3p~!"3mV r%7D2f] I,!?x!QZebۼ Ůz5Fxhj\6.XAQ}iax6\v0SY8Ll\4K%b &?ONj-Lت0UlJr4FyR dGtV?d>{YUC/Q=GLܯɾ9qIݔLoz{{TC\ʗRt3-?|WW\ܗ:kF=^N{ ޘ]]Y[O#6KaqɌ>3sijk(>;n^j; V M~5E>}{|P`tt_Lyv#Yb":唚qO%Vx.sqqO C篆ױmg49 =@/Q%Z}dX4Di}܁ⷉgy '/ CO-G1ZU~5|^`W+`4Sk% ,U', 2D"38x;=9ֻ/)/~֊M[:1#QEm_ :GX[5^&l;?ZyX64+f1_vNn>q">g``V|.ҹb9)d715k8|XU:7cbڲX>K>>< oިSh򽼗rr1LhYAݵV7=wЏ7w(e CݪyFT`P>@|5|]ׯWl|nɛIn ػ῍5jMM7ܼ 9~f+M5/zw楼r=ޥY:ª_>ŕY|/\#r5/ie1TqSܻ;[ aMFXǙyE5M!~3l^< |gz_m1JBOK1w ?7ICnc<=cy~%oՐj/QkO @r\*^ǯG~5|^#x`J`7+ѓ$lDr@"vn=1ܽ;43ߠе{:80;=?O| 0O7ܟo< \)yo8o8Oؐ{TTz ̷V< TBRi@qᅤ:Xzq{.>[=+b(O[Ow~5_HHSƭ}<=oۛV<=7T7OU7noV`!NdF $ނ:~ Wd!pdt:alCglVfJb@(.!1Q1q1BBu$ѻ\4~4$qi  V\c3XqMiǙu7M6/[4JLDP*3-F3aRBqQq&LcǧO.CL΂# r^wE ?e]uW4 bCP&ꒈv' `-#4 (B1PaN8bXllСޡC&VƁ=aOggS@L䛞_`a[?XuL_(o\ ȥBxb \]F2bˌ!"%2D D)YD$ Dvd:q*68{qL-qLJLQ[ZWܐ2z2rkVGܐ}dtSL%("ZWEFٶst)#8g؈Xا5A2D.EE+i4)<P. a fm$g1C@js=FzsH@$өan1`9^d2%a5,-V> ɭc]Ƣq\9r}!s1d\03d\M9%t\ǰ 03 UQw@@S;7@?~8PHiz#J.I+G^SI.]dq@7䰉4=҃69%3=tݵ: k(@ {uUN "~m:]6@b@FFřo&W o!&|/Kl`oL^Ÿz} uJnw`b3,.֕yn$:԰(*$^w2WtDA/"kQ,n0Q2HSL3!L`izDuzG =#<00aNdCwD#:}GdJVj7\ GȀ|Sv h$ 7Y4~7d {Dn7s(u{>˄Y07lr*&Gd wmْ$>2=3۳}奴m,S nB*W )ݎT$_&>w}fwXBMWL{4vs\ YrLnI1c#"I媊PY'J'Z^GAՃg$NE#!ycp %!pѤ4椧Lǰ H  C$B:c;3#; HCzduG&ǩ1sL|8哑xi֚3wϯFCԵ;#[_\~6TH[Քt!jr~i8|bH=6g?Я)z_7  &$F*.!$o=װ*K.\A3ƏF#WPA}iDlU?9$f,~ʡ,*a$pߣDm7GPks j__qD {= —}ogX}9%kѰf% #p+Dl  jbK cC'B NKu9X1@V'6TG`z$R}GeuQۍdvлH.Op+BH HHly}MT5Cg7" 8O# ]^5T5pǣT"R83dvIai1p^V['?\W&b԰7JdC"EY!mw5d:/򢃔&S"S&wsmp1' z?c `Yfv`>Jɔ[ώG@Htv6hyH" Inr}}x޿Ù`נ$ח@k#=0mcQH mP̳ ˝{Xĩ'CΘl$d>띚#?e MZٚwg]k ڕVۆx$K,6SprRraS$?dPs 29y,j=~5D6rfy;*I(QC$Oq")g!.ݾnьA\ָ 8@ ((@U@=GLFgw)*~fc܋j2lت <9!-&KqƭkQyeHcR?}Ѥ|c b^we? `ly7xQ2a9 cLi:Z+W^զ|)&՞ػ7y$߭4PP 9[#Q _ސqo~kf܀3:pO J( VwD}`/&xG`n0vw<ɪݏmafIC%ys@bNq;TV|x“RdsX\v9PsoűUV)0z} /"@D|0-2C(!H 0l%5`Hm@ɦX/~|K7}r xMWcs4E{ F<=f>=h mvV`VѐU͟W .rVKU kIjhtF 9ݟnxT\zOjśuF';儷"{I˃ -g@FݑSsw|@!;*2ټmĂ8 ^5l(Stt 3ӌ?"bszƹp| >MU^`ntl>|AqKsAdF2sxXm*p SG4nτnM@hqmI*YD3@ҠpuǦY5 _@:0<|4R" iܥA6 h X_)+4'o:/ť~`cԼ,;0 hb;\ssH{:}/( HL lhdo;֟HBʭ7RU@;#(*_ /L9ΐB60')eE*`-~~%9-^@pܣgx9Pw:Vm.*Tt%^5d~nE匡~ҮO{.q)SH/"ЗY 7]w9 57>QzC S%tY"w>=(8HObiBο9~k>"zf[$ uܤ7VB|aFk֗_4E13@ k#AC?S\S&)?NҜ %;#Q3 $0@Yܻ~cOOO2<ͼ# i(^pa@%G]BF6uQfTHP}&7/hlGSiK[:jx yB}799_ӭX5؏[@Y1gC9l*lP_y=P X^5 Lj 3&\Ts<0oW[_?_}"\ Po~؁P7KbKgFN)] 6WqZ AȾ7@OUϠɳus5x2g/7ug[QWS|ZƱa Gtptm, z}%R ~m` w>.Ô ]+`SIڱLd#1 mh>;- sGRKKJHz.zD宎sf}$r}8VL1LR4h ԟ7"KX2'e>t<%rr8j%c-${WX8XOggS@ Lc1N^5uI/i!p5gN EϽ !Ycs$ؗ6@LㆄvzI* sT ן G?f&CKx[VKr{M@X H$T` ~nH#o|h_LO~, gI:3zn4@2QH;ϐ"~n_4${3! k]A<=4 jT^l:cƥzYo繟3k:Ok,UKk:op/ĝ6޶B }zL[Z+ښN6uώW^-Fv:p8G{{Ex$SU[rٚk9f !I 8j ~5듅~s92P5{LNzكS 0߭ 0ϟQ<Ѭ} 4j>lHr]5vL @6`@0-21{8 LmofרgUem k#vv0f__dq&?5@͚q,?6 @=mNdg+gLPe{Gh.LҌ!gߙm)?hcG,LDZ4R];?=g=WƆ{҆?і\fN}دI_9͟a}~Ds,ϯ+I2ZaY= h1 c3yW#zX؇wnge~5H-5wņX?uo5]'뵌GEIVT~53v{@l:Ӿy`Hh?Pn7B_dLxi@AgrJ0B:~xpɨLwB/+?Kdk):lZ㽰O$}'YkM-~z|_dtv46cb[9Or~+\2ɬO5Vٴ&iqT^'3goĕa\lok8VkusjY^+wfEZ5|9i W֪NQe56,d'9kHތ WrpP?'pwWuW8묜}'mhVڪ|*l$,~5Ft7r8$kxNy|~1|Ɖb ?Nk 0P=U'I# /J.t|M+Sk{ceė'`y=ߛXǾ6Zj ന-@^:08Lu56ä794I;#l!)רs`Dq IV(ǷµH#vL|&7(=p' iY[+ wL4g;"|lZI&f'V?"[G62:hnlfKPLS^ī[KH ,4 9JѪ{Dmj@] 5Ljϭċ҆g~5F8eT[uY'Sv}69]?z~h˦_g>sfqQen碔X@#8Kk<.|P jR[UF~gxX6m:}ԆrR9kowIy`d>[ :۾ Z+՚+u? 56 C;M[pedzB\)9O?6[[:O7tloMwp>[Xܜ9ӐO\&3~ˤd(r$~ѧa36?1;ӓ L~OOH8<.=ZY:Ƚ Tl#Ts42CTg̪ 1[ ~5&Z\KBpn+xw ]:ܸ1S螨9qI\b"IHf>T{_Cc J$ľ{Oˈ)l@ 4eւ kH^<HK@{J{aXOy| 5zǝk-\ωAagaҢ/,K=ѧLN˯M.^?~zET=֨,UlڳPKt,YQ9ʚ_O?.y]E,ϳC çɱ^~l2X$_&xMlWdVdfbJpxg@ttwE媐lcs snůczk\Wº$ ~5^OGs*]5mO ]}ixyw  @W$ᎉ>u ߹*~z_iZ_vvwn~}m@"6lI-5;ʹR*>O.('/9p!rއl{ٱCG}#>U,1l\#>XlR ы2PgO_dY$t;g?Osd2r-.+o(*v>IiDǁ&'GV s_PgmT8MvJu๪O5[b+>ɘ_漬48/m)@L~$A gOp;mAe TX-Ֆ~5|]O:4joPg ;9`Wι$1 tJ xiV[oӷf۽N$VYj}& i>X&@~e]ҟ9yrbvՇo#f[KwUAIoP~ ,st1^/m (750j])`4q]>7 hoBe5 ..8aX[{;~Ύ l3V c ٸo۱ %N<`F~ FזF2rKEϯHX]3bI1¡z 璝 9y Cl^&9 H?OQvIu|Keti/ʜmK螳af=n Ń~R0=OΰM[W̗޿+V0$->{UtPI^^շʽCi\Zc䖊oߪ ~5L^"$ oP]]PI=@O?߾ߟ8wx<#s=,K#/ozzo_.\,G@}O~o'>ݧ *V5<=m}O^y5i+Ƨ/]?m۶ڶm蕊3.$HDEiiP(**..&&""(@$s]קO^k8q :xE%x<quݕzW+u\.EMȰ%,:Ӆ KX<x8##`t%,a \u]uu~E::t:N1?#Dhr"?wrF'98x<OX¢p\~ OggS7La_b_9HUcL|un5vӢ"2kFJUJ0D(ZLDE(!-aZDTŁ:p֡鸝cŴ7bb8rܢb5M0vw"P`_7,Z*K"9A+S bb B zCRfG`qp꽞 U]").>LC%Tj# HNb:^ -Huٮ'8"bExz a@&:["BRx'Lopp09֮^+˖-!4 ÐY^7T(`v7䅏2F/!DQ0>"}@$#r'S!@2G [VIvXO)w:N Sx+JuF1I$nW.K9;/a]p\@r{aqptȥc@<!ko򦄢'΀ɥש: d!htt tAah"Bc6_u>Cw4P)o!sp C uh\uޡđpj- ]OCN< t¼aVVCUE%Ng$lqVRRBlz-@_@8Y 6$ RNgbJ[DNE-r #uLSBr@@P fh$~7x ]1,16֊6e~;>61[ 7}Bhq hCI޷>+vz= 3Qoӎ )\9c+Hr5ÄCX%~N%&3Vk6 7x9̫8Y9J;3 "4EVM/s>r/bKZ]C_pw;dIH\l3N ɄU/@GSK tb;׳0)Nh{{s`t>!rE a<^'MI {B"͞LiXJ+`V|4#ORozβBj}Õ[^W0߆RWE'-6]X1;oo <؄_*[@Xmт@h{^F4 2WEqN/t MhjWVEOƷf~9t|Kќ ^>, ,vn,G BcO 3~D4b.ߌ;G?]${bZ6ssdOywG'=X@S9/g@?73YoZzl_ظ_<8Kr lhAj?7&/% {RJT;|U773Sc<f}=i|W4@"Ē J~rLY{=36Mv s`YAN6HJaU§SmӴ[`I$e:3OSjs ]<حsT fd ]V@ ]ArL$Y͜cXɰMc-?NKπm:'TŬɺm @pxcI%h*0s6r oye vDb_9tmGN ͟|[GH'Q=`PH@I@} @"~5}D? àk3Ȥ|(z#}c{j}y߬?ڀ(v 8@wԀs$$ rh Yr>C6a'gm;QTGtaimgޗP»C Xݷ̀r|T\BM6OI0i7d44F&hՉZ>\p)O]);S U,"s^ aVj3j= J 1>? z6y?zvq;Jֿ}Inp`r>ܠv̘I,|=ligި돴Oj)їJI)~`<&g M~j`|@Y oH4`j% x h2ҏIhzom06:l,+X`7ӹ9\K=4+k!T@.>4̾²wFt7CK.%-6#{& %{WFY%d ZP=oivSOݾ'4ug7 lИy1a~!!BKp0pS2Yµ1粥OO'|P~t;t'~;V }GᤅA^$4:95^Of&z'q\Pns&"]nnj'f  HܢګE3uU7Ynneş*= L`11'hoaX\ɁZ;\xzIY/Ȣi&/,߿G@p9}=et$\j5C~\g ڲvF2t|xF?|!SiM*nB1Ks:b˫}j(wۛ%DN=đ-3\[~2ҒB&A (pGfRRD0\bsz[RxoJY8 t9A){%*^/+QUI⎺lpOggSLBkA}NA U(ADb~èp5 '~h}I kbjn0(pU&^L)gfa@IW!Ԕ#^՞aYQ#Ppnm7 jr34@F moHjg;& ؾ)P@?^SS|a`wIc):WN.c_t7o\32:"h7'nnΉsAƋ ɦx>i䞢Dh'C%˙߇FcW)qo&vNf|\# Y^ۺ&>p;EpvOV(x_1k*+O܄EM6҆\j"8PY>X~5gln Wi~!MNPHm%p6v_6=3/A­a=ϵPklgIL&% d|̆Q\a/HlJݹwz^_] 0X~uFc >O%*SK.k5`6] j2rmMa@5UKS7(xw9@-ZNJ"%JϨc굶9&mԑ'OVhaӬʟO%SoUWBA^ (2-GB-=k, yd,I%#Vч"/&gz ճ,j־c֛Os?wHZ0 6?g-.QB@&9]ڐ8[F!p}eS_׋y,6r ~5\7>܍;5NvyI8(Vџ~m1LW3Kn0-ݙŌ (H~2_cIy^r8Mt `8 --D^)s3yDD /waG;em?%{R.O).xQ9ym겗mnOR|j+皅Ϛ@8WI) 8іp׬uc>tE(Q]+k3vq.p ;Ϙm@?h} /'{kt7@]:?G$i:a%-=_uF?M;oHRxe kOmޟ}ow {Dq=\OeZZ>ыhx'Mwi2Np2WOk'#a _!ǕwnJ灇g$033M%Oix$:i@nkv#0r|\{J|{vJ6S#pM 9 V' ~5<'_ni5kE)F}>o 0ۗ]QXqW"$ߐ$G{{kM&48T=0^|*O6 @BѠ47lf< a1->j}w}Nwm|*G GYNRg QZg' OqoOC[6W.R0}S2?[9xOyk{_g<+ГO>PwF@VwB$};fr  $FcUrsww=3w!:>Ikey,ΘjSW)ӷـ~3) o% o :r~AeS_~u6ɾKk?oFx*xQ,5 IU|WeB|=ga G 5kt+1EN$@̮ޓZ3l ǤID6ht48D,dl$uԵj_To8"Q0OV&9%.\.$zә0ч+q`];^ktt;}xvsҙ8^otp1^<$WyRz`{?EmL}li/넪}.{횷ҚNX&w҈n.n&͎Nhswo q2U*kza{Ф[==9Lw-m(vje $J! Z멵3m9qw[(We|7h+ |N)0d@ _$+e|9uXzuRi61}cy¼,1+mb/l=yQ!W[>Vp >5|OB@Rw5h_`(d;L^圹dAqY5 vF&57SץqӴѡ6kS*o|/ K}MgK1R罉%}e>;;ev"*}!ߤ!Lnu5gΈDє \go{^uך$~!'X!N÷n6[ʛc)JJL 5RB1s^ߵa!,GyX=VM;A={Wo_@^*T{)oPZy¥~5|^@I/lxI$ DÈb=vN`G̽<ߞf;3}ϷiKSu=POrkj+mt8r,̘os7_Z; ܧqk[ȅvmP/Ԇwc~'90'k#~;7WG'yX,b}ooO nO  q]0d!;rx#;rŎQff!oxWQH0(z&'b`pcAсي*NE;b'a⡊S(L$45A ,$w!# <@#%97/h-yfMCi-eTT5HRQBc[jqȁ΢ښjq両j73ZNj:fo+iqEUOggSLUoqaaGQpw ;r?BRP,U( +Ȫ Nuf$ #&-&%)%K,DXDD -޴qF7lՁ|v}<_,U D|ޣ22\cEJT1M$s %yD@DLMP,N8MP:rVm-NةZܨr};SK2]knTiGzO(%dAF*EYRCQQ 6xq.D:nM'$Yj\ Lʱ<0҄ ,"AѲ78q^+dIXEL`0s 21x}uLWc2P}XJAaj1EN@ǧWthj8ުqe>H)|} W\2L&: `?C? _> t:t!1z\/~3za, a"\00׼ᑹʧ9ffB7TO}*2O)oLE eH&ڻfTR)5%agu鮒26ĨyZaz?W1)2l!XLqmL~Y/RьP3t\Qrb]g0 C`L-K%duX( !jut&Su(v$+c*n[ߤ ̄X}Jbb2']d „ BSbR3pTABB}C#(@-P7zPC V8*:\W.T/ !,%`60[7D$ sDowLcSZaģ)##d`垽jߣl86 S 03Y+U7W}`b('>W_)dKT-uE4PPErs-!$%6"^vÈMUxGwe) O qj0`j'x5c"XUYv3";ND0xw3&3Ld:g$ s`3ha"NjC: q⍞ч Ck[lm=q699&X F~JatN~E)%׼/axC97~6qVd6zX6h?ʃ6^3#e$ecԷgHb m9u"L\o` olPe;O2 zb56? TMӪ")cc$xOnFEY̢91H#Q0,.`8_\"ocYj9GU"` ZZT\TkG..4yx6aCA}[-5-H 5@xj3!&d`uH3)`Pj\zFb3鎶n$. *Ȥarr(p j]Jp ;OҮ7^2^5/a1V.Rjf Igc.]Dy [0v4}`{~i}p7b[b2N"6O@zy~6A2ԟ[zȭHY {ʚi:1.”[ 8$d=AZ40y ԉ;RVv,9lj^`+[oL:sdu)i ֎|,7řzn;uw}:~ z@ﲞT]Qwηn(q8/ei;$DwIf,IX]r!|W{S@Kp <|\V|un5Go Q6F lЀ$wP$ >5%a{;.%b%j&ʮoS0Є,b8pDgiB fGBPI3Ԯ5t OѢh3tƟ v\ta8{ve'mo׬R QR9xkݘ6'%H; ŃQK%$K& Vu<Y53 a^KE} Ys񺬔rdhe-c{n5gwE=cmu#SO/̯$3^ley;,slyӐ;0Hrox,Lޒڥ`|utY=M\ `sZHpPo~ M '{fݢojxhK "ZR?h!'F 3|o'co-{9WpHyOs7e^*$^5vm"I#Eȗ?uCIB=)/VHkhĠtX> ] Cy͙#woN'ӫm$e'1#I tJj*`56'u;L3B'25ڛW U嵰"$Hlow~x-Hy[!C~5 7ˍ4/R(W>8~># >5:Eûxp4mHp^u%Igs2^ u"5pn'6e`n&ٔ,^>Ͼ,bG a*+nTkn. @@H~ڎUi ً~94kŷ\ڗ9toNJ+e !f7{<[@i/#";l2u " = !g~_;p5gKyM\MÖ_ Nr=NQ|e6t|S9q54dH)РM&[si'[.Uv>BYg~5wp|ځqj8߀Vp" `|7] )@\{ $ `=˻Dok|m^8J @.Q h ^ҹd}[l+$?U .hZ^vl a ls{\Zr}nAfTo7dKQqAv֊֥_N>LcUxsN7:]C7<,~p)8vdnVk濗<LL_K55KsB|f^A|6^rTFԧkmޡd[ը̕Gi-uַ}ێYiW^ޞUr~57+7]nI:aL.Vy$TǶ5: 8*h7=%qKJꜫ̱T2U'b1j|Uh#-@%onZGZF@mZx{|BPX*伲IF?#}^|eP &cvt<5qyԹνotfmXa;Oܹ+-.`'1dEҬT.XQ?fxV<??3qh|:N/ic5׫oP쫜oXȣ6-\o؞X)eNW̼dGH~ ƣWO,=j'ˮyU` 湊zUyԅ}rdD![5Ɲr s5l_nG/^ydunp"513`>2uh 7piGRŇ譀MR)RpҢMO"ܹY9ц>oĿ8 i Nh3 M:YpYOPi+ C&8 W.t,ShBテ>+Z)I!7*l}uhD婴dΊ'{/I B , =anE.4{F͝[K/0s\N.xݑ?U /S;#chk|.>d(NmrIJ| $ِ}VN⮀^G\UVRh%b@XNg0h#f$ r݄Ȑ)B~5ގO&{kML>ќxW=r 8?;w|P}n=5\,ID PΉ'gm_U[{-Ǧ^gF9>:ZAo6C@c5;$Lmd3.W6916 nUjr8v^>0mU\.*fTs7bNś&nS'ue~ r0"@3zM\g\]?z^}wL|995!"&/g/Zԯu-4'fCV6ia}qt(:-'>/.ǡt/QΦ ZG)hCs2~+5v߶@qSSfb##wPCɞYO,@ (5^ e|jxh 7}d+7> tw\U%I$ @=ԝfF#N -O(@h\lYl>AHoe \3>^ $91sw,3EYRWKu}ф75^i<6|X]ONX>~E,N΁9_nAq2U<sVW=&vp zq,>(O9NuMQ˚~R15lQL=ڝN{L䖝aü̿ג_>':Facg*r+wcQdW?[խtꝼWx_vP+~5_PҊ WÏ0n6,~<7%q%i"GR yj^LjQ̶;=m/JQESݢF(, @R?xc{'1nnRq`86k}L^˶)tI'/  oi>CJ_YgrMψ> ";!Ѹ! JO/gc~n;7fPm52IqzaV>(yuZ[zn|tcW=r?aDG" @VoԢP oneb,]n-pT*OggS@L!l UCF_ee;W`fv5$ $ I"qLD -1!0hZDFA L&0!2`Lkv FpXk77߀E'X\H ïzQdk"NL&L8:FZDrǢr^#q-Jd"cQ5U8M :]d ab82tI }miM7DY7mRcURaJ$Ih$B%D@A bEhqQXE9N{VG8<~;%Y]QTT-=m۲gʜ'HJ0b""q!b" (EX\@Qbj9rdΑbUT r X [e0!w0((DZ(md>&03t1< +  fZ(@\L -.8`u1^:5zD>*yj鯦kwMz.#V5]HYQYdEf@d9FqBrd#/#⹠ 9D@ q̂Vc*TͰS,vV4PL &\<:Xt靠NE}"WLxE0 c6I|8wh(>nE>CMX-)shQ@jW```x1sz>{$!@> %Iy3L&\iDH CqB!@u]c29xq=x=^븎d]>81ȝ(c¡-[LV(aLiSD$L&%8,S/UԚ1'ƚL4B#Ϧ.hQhE̩VQ5L "($ ֻF0uM.FSşZ99;3 \ѡ5J`+G2+|,u0>tS8"s&\:Q"9DDHo"D0ԇ0Cx&$#Go)| ѝhv0BhCRc<>΢ᚐ8\I|ePSCOk{y*$J[9\ H7~7O/J{C$(R|T6]_{[D]F7Or[ IgAHFosGl?EYy0]OL>>ӱ)UA>#@6r 9ջaVFA0P@\Sn;N K1RnҋQQ3EJ pCT.VDPŪ62i%2Z^Wxzb)#ƅH 2U &]uETP' ̳Hp4‚wr{+ 1AZ$4]0$yhE'=)D2is_A6䡉`n%gP&񓱧%)N|㸃E xb<߫ n]7QA e=״7;[n?ֳGPe\ȟBJ^'qd|8X_us1+)K0̹Ts۝m_aF汔 tCQ"N"J0%tү/,*aD%d%*"S`[dFoFT0xetGnYtGr:}j`\pCē 01|x\s L^l5w,sb^6͔Q.'mgQ&!k6\̩ pG}=/9c89DHR)Hbyu^~W/xۻ9V_M~&qm3GMBv\HŝAmo4JZ62ĀEss3o HkU# !R8 [^B׳2 \77gsYYa[Vj5샕hU]:n=:\WX{L&CK:^\v;.iݜv=jdyݢ=t:F&Z\uGvu2+aa,Z4E 02 <. 6#農 &p2 <x5 #cXmOԐI e(d}uwF>0Z/c`kn{ANzZDL5&&HpYYӪokXz ^r/l(oñ )B: r*OUB?zȚ}6M h*m2Gݩ$ȴسY<49V}%~4/T$JEЪF<-}7R48fl(K 9N5)%Yr|s~xf1]@(ks}p?!HMXP%5ܠP,I1@N_ZEֳ[ʔ;^ag~sҏtk 4v~=?S?7ulH@t/7К@jA@$Dܟtjհ4Xɦh\oO ^^,=\ٸ{[[;N$]n6_wzsAQ@`g(wxk'<*DgPhGi J# 0[S^0$9Y^"-RSrW^ [^5%v)A!/,d9 V|^5$=찝FO(PJ GC [oŐ3zk<f_l@@3nzr4/qI$3;ˤczpsP[{.fP*ׅ_h`\H/ّܽh  FL~lSC#@Ϻ,3k~1/M8I r.9dz~/قFvx'r|_c  y̰-._h(@S:/(FȀ%A|*aϓ sWS %?#ƞ]Z$6|$od–mΎ[y^\` =LCm-x*]Nicr~a'KMUE.^ 2+߱kX=*a"f@>W0l ,#&/x(>pp-4@Kl&I G&AOy[ZLV8Hu,2ؤ`4KmK<N}_C5qlvH]5_@͕Ig&MBlyZ~eF7sA&~̣y*bXS 7Grp4xêb"NBl:6i.̈́Ľ+|dk?3ZIɄ8z/߫ȫ=!7~wmMRrJSq/*jmtXxz/?,^ugqkl0qGNի]i TҺ8);Dz.6H 7. OggS@EL" xSO ~54ɻ;e#c~H28\{@K#fo}i̿>to/ PnnpdJ+2I$X8QI͹3ι >K~ %?k_cmuخgG$A@ ~[' z@84 (CF=?,|}t]_Ł3?hdbj-@){~VFղ^#J>[P1e>L>n#zr3cfv7 dy5={^23,w!> 0J j| ;%CMSL/0J|Jdo725 AO7Dvs0yo%ꩳ˞omˇr`[U *M`w&4]2Dw"BNCk,u(k(\J(~4}WckЈo4 d4_ ?>E  c~I&ǓwI^r aۺd ] GUI Ƣ;B?S;Kz^tu2?dX_'(3o,cv ۆ#^~Ssd0O*4!wډy'D}MqY ɈχK>U0ǘKއYRf9ic gL{_*gX1-g+Ӳļ0 yȻqT~5\7;>ݍ3YJ^J,<&ʱ/łrsng* `rƩ*"jehKX(QU*]Oke(TF52|x & kKyߗ;/SdB?g swM"'1ቜ/]BH&á,'uZ6P8 $V|)K}|YxQ3#)R/yMH bKQѵQI~5\./DUWQ2L}jOhe 4<x)-`F7=vI,Dp)9^޿"~s ,G")hWt$MLo׿{ΒD Q !cYO;p\0"Nw3M|7uʫKwO&%uTˉׅN#Rlt/ilآ?ؓ3;LN0oH)v( GG]PyXy 7yp˹/w7+W?J YH'ݹVI)l\"8 5)jx.oJD{!a1;; WuH(.ԩ]U]?1dwAo%2 pI5Zkoyf~;3m7ڽ7iw"{dCd;LT]њʨDr=uygˁMޓ~eI%2MOt=um kwhqfmⳉ$2H4T ?5a@ !@}Z&o$'9 6N!GOT'~5|^`WL5+'7m? zrR$!C&Л;*ŸH݀$%9ucל F;pVAF?p/cŰU"^D8>O}ԶcM]^ݐjxй;6gkg\Ѳ(YH4g ? _zGt5`~VSΝr,?yyʾ=x y~OeYGMdH46s"enL3;q`==XO _mr~b! lL3iO9_]@ʽx_oEJup:O].p<SbT.oOiq˿;RU>>~5|^xahj).lloDĀعN#2}8ty·w陡>[?qss?(mRڱB_=ʽ~jb8;4Sq[}+x7?rhb/e߱ |N{]'\Wi GOuXzOxoOggS@L#ma(,>C]adIKm~5_%OO>'b:<==-Qp?oOOO7Oխ ހ v5  -0&͋ flLҘ)u^ ^2,a0ѬQd!Yƣd_<B6@ P\1+f8Ǒ1bta,Q_2mZ"94weL5( ވ)tiȻACZ Ln "b-аIO+"C"v)C7v&% ӢZ[xPD(Q(DqbXp\j[qjU8qwG78qwG78ߚSfELmڥ'2,ɡ"B!aMQ" 1C[)cMG;{{8OuQBQ '/l2&SQ֤fVFDKY3lSdYT!@I9egP -!ĥ 0c[juhb Z8O"ų JlKDpAl>Ձ@QR5RV(Cf Tݮ!7Spx67RLy--!UP&rSDiqtaocog"jW渮34L&s9HhL1Tz|c}'D鯬oLS2qo@!4y2c`2Hw94S@&z\ǕL&>[50aMۍIwF _N2\u\aLv] 0aud:>}x2%~8D4Ɗ.RYt5qdLp )%MW2Lddikt;xnt\bHwB&fpG>bU.twC[ڂBNI5IZ]A4:1NbІ *pyiUuÏ+D$c5> Y",0'lD"tAfXS ^Z8sx!Rbg`ta`f <ax@52OΑ:hs3>'>bkP=M !A)4Ak:',R}$Nyuu\Y:@$`K? 7TLw̆#Թ! Lr1.FÑQs,E 0cDR%+s9X =)h]U:1bw[x.n-. E?'[C 'L[кwLe"3;LJM{ԾHP);|IC/ %Q a-Cde|^Ǫ8A6ph%;e84a^? Bcf&j&2,P=aª'P  NM0A0C?Dw0b3 C s*'.V:coq $`TH XHo=Jgs6X04dpmHȔEI(E6]dgH;ƴ/8 hb<Mԩ#GRi; ]vq@}g t4w#g{='s˷TTԨ>N 8euyJBVSSad?UU(y\ nQA_˱a#ql :빂 /Q@;䝣0]0ab@xn$mD@R(J݉نw:$p>zBEޡihnD%h x +p uGRv)޲W܁;>ϑWH |@({v~5TA/muqK6$d+RH"}˹#~{~6O$yh^55`M778p hε$̔#Nu{k~&~8wTCrT!Yߞ2&|ä|TJ}E7۟E&Ӻmd&ݬP;q|r _UfC U!6PsN<&ܒENbyZ+7{~HKYؘ UX)o~D fzSn0mZ5ŵإZ>\vjz;8S*c{J2eNH$ׇբ]ي;y@::}Q6. A6`H[ouASoh8AuZ&Y[AU=_kц^5Dx]ҧХD I/E8rM™9tx}gQ/eX΁a4\&@=sn0'  Gvz^V9/ה*ʔ7f b4$jm9WpAĸ x՛`x}nG=Jep=GN!F le\s)].ţ^:Uy? r @A Yp?:p5*˦$WN1Zcl] ef h7?^@4 l3}vOs PJS=+]Ʊ;( S=U"qS(BOH\ wJIT ,mq+ `9\bnBÍXk8D&{&hIszr63|~s씔 Wߑy_ʀ,N ՟~=^qmD^j Bwt(/cPOe絎ݲǟHֽ?3Õ{/=q$F, PݐP#0V)7ʹv[o ?/ ڑHz#IS-؆kK,pHp8Uw+͐5`hL7c޶ HM8P {/X:L aE%/hI7\EX7Ȇ qULKqB!( n5 qE{{PR vo=z"$/'$d m֗9e% >C  (&. Qޗ/_'k EOggS@L$SA7`T~5و$<ЦGװg0A[ Ԋ"#p w6T|Pcf i}A\JyqmӱLl6kÍdæ!z_@m-xC'73 y @u39b4:,*y@<6yxEd# SS-Ke$ Ci%8spc=dTc+4r`䍒$A XM2#vi ڔVY//@H<ϊ 򈼸RbXv67&M+~PSB{b6|V"{מ9I'9"a;HEv* "^5tdF,iuufI͟2ꭠZ`_r:<4&&eQ:|"Ou|Sg "x+qt\o9N-9C`$Q KLS;]Iֻ,=.2d^5>Q1Ζ^ ϒʔg$<Vo3sp47]Tʛ\WR)YD0')Upg"I{,7`VrfY_ xИ !K6Cw_ 0 Pހcu5W :O' ǖ8IC^kiMK+=4F0?56 @W6}zvK>(,"߼A :ݜ! =dת llf&ak_;MtwVgS}CDɨ!^=`bo(ad_iD|MeBJicr?VIȡM#XK'wv_tj8~5Fd}]\/J~5;f_Nb\ S.p~1Jc8/u<Ė<~jX Ƌi Ŏ@Y"6Z ]NI$`Fb|t=O{=^U˲]5:dU׌F^Q^ DKYN?oۺr%>'&O9D#ԇݞ}. L#ݦ&9Q3U՗g[ZeAw[WD~,vgO}1 {4}xRk]K}5ՂcTw@b#lj~x,3*fwL~5oN{}fW}pڇ9eOra/ x2G@4WX$EJ/e2~{zs\VR'ࣦ _X7͂Tnlu8v&EijȿPSEQ;.6_GƊd7L!(İUDN2w mdʲw.2+N}Nv\J#?yf;&2fqΏcߏ };2U~tƕȎL{kZ2O93 ~% ƕ^_ƶ)ZPmk=lUh͏=D5{Omnޕj]&7{WwxMp_>L 5oPF4 U$YgHS>l VFsb3 :KL-f`HBW[C7Hl <7+F{4->O1ln >Ѕ'W7Y&(dl./=\,wD0' q])9 l XJ$-iLV{Fl\{z'Í0ܜ@}4L[ߚz{stH":\:a7vݳ'z4(}R;ξ.6^3v\>`H&v%P޺O3صbSg[o1-[ɗ@Ϯ|^J]-ݴW;G8m8ρ)~\F41kx.$&c*2׸VZk6> ]ϋKY82 ۫r^LߌeĢ-@mA6DzB1AmjܮƁ ܮ33S]6̸-?8ޱ.v?As;|g_5퇍z_~.gf_F(.+>ch`$.ss@='K^eCdD@aTܷ\HgќFgPe79J6[!DGyIw˯n (H|<)lGi1CӻI:sF8Ov $Xq-P]$~5sLY~ 5Øo T<)("s9 F_,YD6w ߚ_?|>Ӷcν_]srjةQnK=XԪN2Hnȷ[߭6EBӥ/a^/5\4;-Z,[4 [[>/jώ3ݱSQ^x΍sb74Ks&}B/|e\ &>$_hd{i,3lް ^"/(BB:>4ٖ,.Ԧ>(0#(+yHc]T| Kr ؤ)]P($@>5|^DH$B(h"bx6b3xm7% *Gu> 獰k zU/zަo6"Ed+Dts<9@ZCU3@O}X6c̻<6/GS_e,%{ uΗV|*efl_Vqs%2>gXVܚgdeh[6Bz 4z>8l{ֹ6ⲏćt/m?:\_$1O &[ٖz:wԜa{؏SrST+~~5_T WÏvHXhX7>\P]8b , 6_ىWWBS_*M[sk'K#!u/8^~v z8N'(NyvޝS΀?~$l /N?aH_Q3pjشa 7P^@,>T%Ǹ L>'K_Zyyid0bl`M7)~9 Oc3'BA4ܒ x3HbmI<Յ qo?cf@Mp"&lHC5|]/w/`_@uc"Ig}Ql.Pu֚ٚ]]O E)<دԣS?Q?5 Jŵe'e\SJkux+r~9xk 9+HU'*},%,Oߣ3t l\}< a;Yd}ωϯc͂ .9ŵT,V0ýpoP:>=~#oji̥\l,0N](VRi)y MT}rUnA@OggS@L%%S# 8ABcd^?SZ]~5|]Mkcs%.$E@bbns#.3|νiaP4uKb4}}{'O_G~ ]vV{UMbẘ#NXG-4~K lRgD.?C<5Ld>sY!'JDD(qiZAp"EupaAL[U|z} 0b[&O_ld7S,*CXH:;BiuHIFTF M ` ZDB0 A,BJHQfS:bY1mxQ{Q?Zm<ߣBo+SKEVTeTQFuQQ]! ,J)ʊ\Y9#d4"$ &.ֵ`QR A,PBA* ^Y5Ff/WT<!v&a +!`1drqL S:ǢTHN"`~Gr$F׏~huL7$>\>"|〓xdp Jz]NE*[ۍr=c +=A\9%a:\;끲"8}x1D Q'r jWJ#0E BG'u'Մu"ŀ\#TueSӹ5Ɓ!Y5pL(v\dc()KJU-ZPOa0qa" C&I =XO xd@  :#/H&2rDm4LR'@(,4_+a,c@Ҏ67$H`o#=hi)aյ˜=&Y;A{_O=5WsI%J"Nhe^eS_©!FԻ^;y5}l5| fEӏcPa"`B;Ur;Sm#pbi_݃CJ]2e`KaZ諮 "RlUD:r1#vtCj C dA="Pd2-Xa u0' .sA0B 5.QUlNUꌒ YmxUfxVojPAr ^6T Fx0Rφ<4Ã2(njw"ĺF6mk;^;:s0I1i~/OD}άA.)47yϓ?TFeA" R^:DΣ=5Kz8O|˞(ex̭R`eQ*Q! #raEWC ST04xb!sLCu$|Wr2Hd#åнj(ЇV@Lݙz0L{ x!u:1t#HE"a9X;>+d@b'6yY 5Ē>!oRW@#+_@D6C9l!֪g6\u3vTH><au#@ Ĝ/(B$7 h`0Rza $j J#8m3VS[rMrYZK\': |f*O Q_|飌 _ҩBR40`6$;σoV;rCk<74˿^Ahoxe?9S9ߴ^OsrVvNr+mmOPd/FfG %5vѤO>=Pv܉gLZ7i4}O!=i@w|pPWo ށ@Ra  4ȐJIn;I"w;^c ȗ@P4/F Mb @H>[%M5]*2wVhxͨGMES4Ra h r4GHN$a`T]-X|9#h FFS-и(jHh DHMJWYxQLd*D޾.1~HЇhHg ;Ƥ9G_GjgET=(tZNܡ]C ֣, ~m],cg%ws|[R, +ד~9c[Wy%~f倃gق5=WDtcFO 4=^~Yl0>ˮ3h9[G" &Dl:8["{e^ ʄ(y}h90QZOi愣j'? 8(Pük >u ha%c-XCZѬA 4|κg-{ K Aƅ*\<~b:+ޅQ:a0W 2"s!mxid 8!i9g9[ ^APp\\,qx0RՋprgYN/m}Ww\{!G3jn<4c?GPW_\p6@OggS@'L&?v\T>tw,l(x5LI&H W10ϥ~рn νt|> \dv̤LV)dVOZC> )w6tυrrH5F `,bkޠO wr2C 4drN4?a{4plJoozgZ鿇uyF_)4Q@3?K@&I <{gfޔM_dفZ1Ʀr }Ec'g{n$I9j{}`73#y)sw+G  9+xE= 64/,S-l_J~5 v4\ I/epB({/ { x|` ؀"> I)d%qj;UOA{sԓqvi>m.ښgFUB9ˀèkE%Pw4 n¥ $PG; @3ۯ~~ ,ײz~g/@4:mT U?Xרw I3UWF߱c񫰳c돵R#nmO^׏9F[ڹ;2q|%>{~@CMw4~pgs 96k@dYygYVi9 oqo14{mHꬑ~Jd.zn%.reZ)*EV}~}KK}C∓,|'Ec4i0m@ۋs1X@lxIsYfjH /Ȓő [0΃7:Q $Xߘs @5{HwS^~. u2;[;n/kKԚK@!rJY\ȫ|#25?} e\ &#;!B\v&kC}nPN:D^$qz?h|Sl~oc6.iˁd+kᖘ)99}Z5Sh8>(ۇkK_X ~5{r!k8OusYRy6Ӂ-\s~e' j؀T=q$Ԭmm<~*9-zdx>BrL3?D0я_3vs@>^D*Y4 ^wux=6W s+餉s׷GfmC>+h9KYNA 7$x6(.ViϷ{2xQ~5߶;^j ٜh0\W3{Y@3{/Ph:IL&Ib#ZMW# . rƃcYرdR[y X.&~ߘl (8ǫҵU(2将&aFw) ޿%fM^O.2Kc{^ook%Go9-mwv.O v,w|y~D9IwdlC~y1AEBE}._;.%~\f3haPAKAO\:+þw}@=xfqǤdl~;] ' wM+oR&uǚqO>9_վO~~5\ﰜjޏY\>3\OM,1~%t(E)ݽŘ",VK.}3P ɾ5` ?ŀ/gq5߈Im"1 ~5@ `mZ/_![\Eth=tƥdv}Zg3y$4'lLfEg^ģ~74"Ml&V5S_߿@L=|ؙ~AK>5-KT( ofwQ0 `ٜk|>@26ۆH1ďlҺcdsd=|fbo[(y~QI7[̋1ͷmr7ԼGYoajXP}@>~5oЌ%Yf x!{J@'6k>W3n=U;jW9z#RCH >5 >=ߝYyS5+s4Ft$Z ^ܵ4?_>Bϱh°+ȳw._-OG~5E&^ @㹺jާ#&>M` ŹcaΖM3^|ɻ)zw[o,Xnx8)ՀݐPKQgv+"j}-`bφv ~f,f~¶3TN#>Zw $O\VϊʛOMF\ }_zѪ^XrOV`!$E@sh8Ѽ2/' kq/OR6B}_@*vCImӅM+דhe4Ĺ}(uC.hX۩EN?ւZ P5|`WÏ+`45 ^`>V`:$oeա{ʅdVl -`zfRwuZ2A_Мpi0MAO:OP:=gۉ'v_^#͇7{.{Yp}7|3q}}dy[b8tߏ3'P3nVǨ~9ڗ;{tݧ~5[ hKέll:ʣJ1XQ(d^ =6=6Nu4AsEgߖAkpNjMn.P_y\dV+5|ގEBSg6@h>vJ%% -}$o߆-lt߳itqۋЉqciDlp6^vͱ뫽xqQ|x_؛eAmN{Xt9,P6;o'ه8?1s9{"'M=ϯ1:N;ay8tvf14 ?|㈥qTڤ<[-N!:o㷿b߿[}z+$T`fZ؀Tn( },?]߅OggS@kL'8 ! J=E\d^ce^Qaq~5|]- rY*Qbl&R$H#Q?`lcǏ~p?1bCw;g~g/pyp7O?ST /J3co=0i𦔊>.O>47{R @aWr[z{ [m?Qw\1K;1SjӇ=҆ZSqǛ'MZF\sBȘ &A,€"""SLhZ D 'TG**)bogUC K N=%~ppL (Ivf23=iӶgJRB q@E ZqGv`:tСvvj؉:T d \78 oLBi{BQ&'m.= %(*%-Ee("L%1 ,(qY@DP,*0D`81dnUjj1̭JML]=N&Wr\7|N]&ƼC-PjH0& ?NOc q:Aȭ,8#1XC:E(#<.^j%"&ɱ @nP<%mC9 7$Nn{$`7$ {9q0]{* Qb{pbDd4\T\%1@,ZϮ,[n(ߗa'BV)?s GAq|,„@oٸ-2zq[9nTj0_0|T!MT'O{IWg;,Vji(R E]: > ms4N ZPz }9 ,$ HpQ~Tn^7hDDnV7Ctܩ\hEFy^$@{N*1vtM$̜ wVRAk0ϭ, iGe3éZSR#g@B8k7Ж!3@u]_nXjUK8$om;zsUqE|"b9ª&-AAL:χ<r}7wUQq*h#g̡zj P뜺@r%XCczt0P̀ k\;>آ6ׇi(NrHaN8xCxnq"hz5EoRԻ|HQ`.UUdxj:~:9T:'r :feQYS\}z^6D fRqKFd~lA#uKզ1>#\'^,4J Pĕ""]XHߙ0>L戮:j:kcx[<> !v$wNORӼ(Dh Cཞe ֦͖v6!`%aV}0N(K>q-w{N|tuncX#bVٌ֦;ìOV[ξT'w֘Y 4V@ZjNgFo ༁PD|$gE_mB&922V-Ws7@))oyle翌Z'hWdRR- w@؊C-^5 zT)+UC;ü0M`[/֧֓t /]hk؁%#5p;i3SXU0ؾrL<]THVe;B;t_a L*h/pi/@}`}ش ` n,p'j~BX3U`퇯_P<Y6, gk%/a<Ѝ3Z{~+ڟF!i.{TM)*C}/AM4}"1lFY^_ZAAIbrp?tdpwoV죝.yg9?<^Θ?l9/v?͉ŀjJzo9U43H'];Z]r/2PK>5T=pʭR5$[7IFʊЁpi>rF']@԰P xĦƍkq$@~vǧ',4vK[)8 77Au-:˙XދFN]oݹU4@hy`),4ճ/4xH@nEc[g"Fs$+4/ّ4L^~۶/{7n1M(>Ux335>pϧ]Q%@{Va.أbG V~؇!Y@" hpq&d1} \[~S 1Fkx 8T(h%77gml^LǺGe& f?5dr߈ewSUbt >.cT3)7_WG{&hjhКxDwd1W)hJsCN:NrdCJʥ ȈϏD-^FϷI\GH^g bԅW|C2~(2KOkʽZay Xĉ bC !M th(Jx'HSt `sH <"lYspΨ߯P@P^o :X~>h 9Ovg^҆ď8OoVgE }Jd=F|)_ FD$/S|*k梩4X(wd~Nra:w_ܻLqX}Ѥ~?>eqro}j/ח\xno 6?xį+<(/~5E7# [5Q5L.-Q E1N?ɻ@W|lӆ8Gu3V$ :W4+Ym 7Mgf| m?]lHpfV>OE,ژhrski:6vyfMi;8l~-O|%8~_Udx3a;x@a`~ߟdk. Y 'ss kWpbLp\skV}s nJ3+ٶTLHC?y![ESZ${v)t\&lH|m*_(^^rNֲ$@>,T[5BR\@〯 gH{^5랫^?)g)P_Ͻ َ5^ϺF=^~C@ 5@ c;.v 9)R/w:G8Bvxm`BNG"t!4_V @I v[ Pėcy;Z4ZPОY]<$`$_'É#aoTZ {irAęf2/Oo}\r_f%GDXDvܡ_y9_Zi+y|rtqS|_e{|Z^޲Xhkfs kq)`QϏ><r4g}æQv2O2A_eӟc?*J3c K^r1 6 Ȗe?+ y@R cIݹő~uJ}tn\P~5=f'qf|7&yVA_Xc=ްmi&q|Ts\W3c˯-тׇu9}h[rj-2\L\uN6j L/Z;ds_TP W9|nwǨt7vtT2U)5yȜe<_\ǚz/4Sd zFHΉ q7ehhY}̗Bʭ譅Ѭ%Rjy,>gKt:znɒ3[ q'dE͟>6_fW{MS gƢi3!N/琾U? ¦<-̢aP2x(G}@\`C PʕH| ~5\6p\p5?IR'"p7"4i9B܀:8yDIae(+b,FyJ\՚!W4@EV57 $/tTE%7Z|6@ڻHYq:i2` s_&zi<y=&W w?w~<'\✎uk#f=K|n|Z/=GB ?\/.B-"f/j{\]O!̗xL AKDIAT9ب7ͥf;~zoi*咱IS{9 f0Y!_I/G?eG[9-oVq2|~5v5_=1~vR;MRܯ%싦'2e=mvcxOJє~p|ށvtNV6<-}.Aqx]z>rdޔS:_H*]< 0V[.kyI֖?P&ԑ%Ζ~5FyNky~({T{3r {h痰o8h> LET$  Gfժwv`(qԏ4yks6 (<Uﶳ絣m84Z)m6,v fӅLZq]F;uu6}i}x93bq 4߾er@5PW79A,΢DgyvӏYɦ!f:}g?f$s·(.ay 8e"!/[o ΌC;.'E9dxI)lN>JtϜmh4Fb$7Ro$zax6>Ys%&hOj ~5^ =LMɍ`8~eퟰ]zաvD(f2|޽[AxZ"iѧafB>~Y JGl>Q|&\ c';lk:硻?j(,0]~Sd+u WaJ4zR>+a$3]{H{,Ѭ\ן"::~ |,` O6X /ݞv/qd3ms=C 4}k+Yj=#ww_={y8owv\pf48^I%xx?&F!٨0_]2 5|])jWBW,ô~w8$ ܭ2__vD\oѦ]s0̭1?&x(g e`ٳ  c\>;n :,]0oxg% t:ϙLWRr]*:uɷ~tk}v=95vǿ\?}/0*m\qݼ&]]NQ|W 5|^/B@6{5Z]xM6@N\ +H(?6sKFsOyž~YS3$dir|L&^Ot9<ƀ5lrɌ97m/]Q~ Xv)(`pOS͞{_EA{vW_fz6%{doEdů 'zp6e! ̷߄ЇHe*͏YCSsnz33(c @[;'LgۖM!Ę+9'7AXd,'X 0iX4CBJ;Z[(POggSL) ߪYI%]cb4YOn5|\p|`}AW]pŠHAnͶo}6ʈ|/ꟻմQSF78{nݗٟK\ruN]RKgٴa߆.Տq?bvll*yo^,6rb31Lj'zr/(onO淭v%//KѾ`/?%|<ϝ8s?1{]Uc! !RŖ[j_H9?BbT.Ou/MӸx<[![m8Ty'.P*( > ~5hxXb@ I@i}=0=ϷߞH8?K\,T޼v+}zP< OO #x+VOխ _v5\g˾ɳe?yҶ[rSLaZ(*&"ę̴P\k뺮뺆><Ǒ$I$]X !Ir8$B%,$xGz^:;\.W뭸HNAX!OLDZG{c.Ř{<~)$I#&dXbAt:߅'&hFB%,at:1111151111Q6;!Q6;!ٯ(UR9snN֒ђ+@1@,Q4( !basmf:U&Tm1$ FnwEmx b{/,EEDlJd g$YJ!""BZ\ F{ajkkvX0MbqGl T 'fP ]epa6 D QQ(s:D$QUMB B. B%BPE(Df0@RҢLcL+zD+mMxwMj'@msmj"*,ʈRd 5PtYR*RP,NfJHfQ1 ,]%&M71&०]c„ zL.EСwt2buC/2LOE~!}轃NgW\20-,@&rq:z=#9q0q%cxG +IE= zp:q2 ̄A߽9-ְZr+>8Yj a:>p[=r5Z[2dLߕ6UwɃ6bܔHF1"bdfD^P~z/^"(1^!/y3gn* ԷEcPy5 Fy #×KovWTNRu::2L`X0:"$r Ԇ-uJL Qc , D'6 f(րAm*,  x|j@! 0x ab„\pn erI~bk^\te7$wx wCEX; I5F/FEp7 Lo WSW'N50N`үb@6ܖQM sdmS*])O! biYjVc0ݺdU$$0,ZZ* b-NVka2nU3>Nj^o"%@wИ[o ]h Za;AaQHAD* s8UmB1^' 2308D8i2Ĥ!}dH B|DU}2TzB8D/4p)m (y6$K&CX{(崡b2.̹kO%XevMTld]l[эs.8T[WKr8}Xf@I>=\1 .~A{[^|}z%(-טinx/ռLYxq4F ]q-b菊;X@P+_ 'y8q)a꥞ELK),,,rEZ60HXub5my9{P!z>Dr(:w0aIJ($+a\!zrj̧3q+d xh<y]*66T霼}ճm'<%2e UbET]>K!̯x56`Yd?7؁5zDHf1Xտsz8qW3k 밹|~#e1pښoFV;?/ f CJO*mcaLN*$~ʅhem7* @tz53XTQC%Kr;b| ^Ͽ9ǭ?Wŵаzr} x ysHP">К܋: #7֌(ʝ9/Lu+'eXjTn@q zr sEC5ϼg Є eUiU&QINOd5 d A?sępU: ,FE'ӌu^a]ݎඒFs*of^v96@ނ 8¹X~ǼX'Q9 L Li [g'ŔꡠiB]vs+//b<ਉw5D5 ju+B|XJU;P1]ر: :ثifk PT n5j4PpĢ&DłoW{{URtK\ƴ,04[5b'OߟH,Y~P ӧaNгQ(όL2F2$h>`sf ew;*ؘU Z\a,R l7d $S6ZF ~5&_@sT6,%:{p K@iH$m2K<{fi|A/|; ;)+ǝO4 =19rxWN@HﮓOM=8qnƹ@'B=v^Æ|m nDy[Uv22~5I#  qk(JfSsd$;X?Ӂ\,եn:jIy\G Gl&I$ɩ =o|_~vdCM \Y(•6|qxnU"u~vwQyX:" 24##$ S@Gx__fttϚ  %PƟ(>M*00/7XsSo7} CxbI*u*RZW_ufY# W6eKY"W1 j OPl|Hz^9x ~( CR66iknfӷ=, bS!'2P{ݿ0ikw6Hdp];\UOggSL*!uc<#^5W>5C>#IP,/qfn>7Pl v @@s ׀5@&.I$R$0qy(JnO7Ck,頬Mijа&61'ǞL n/$@ju^Ü'(7OLзQӗ!Qpn4_001F U-`H3`G[8ʢ%kAuUin?LJ@^{{:N늰Wr_읝[* g,F41v& ʤaϖ _RB6o}hN-,{̱-g6ШSu57Air̜Gle5rU<(޾+Y4 s98''QMc}uRh9Mưs8dF&Iz,F@@c%N4}Ԕ]"4.{S|i._j 5=ut\l@qyJ,C)gb4tkӯߏTqw>,2חՀb  \Gl*HH} x20*MF IuQ@(u.}Qbd(#X^h_ % a*2ĺ6$+ 0/kdڢ3r,{3\Ѱr*V5u=:s\JZdVlW,a~Mdu;!zJ95IHw1}pT Cěc*/"}(9ا 3 Ƀ\C9;ַ®H~5p§8tKڜ 7{5}BquN4^oi|OYzNFW$^9O'O*$:|̡ Bt[ѼM jmA5T%5 %TӴ:Y:,/}mGj Ye"3nฑa8 ԣ^bVo'$5 C6v&r'qz#e'Zb+.14/Mt׸ukO/E}SrMɏ3m$;R=F,.)$1'i3[l̪RuA_GHʤ.,2a7KOV.(~5"L/q!k/ wJ|鵇Kt9Flq@؀tybL$ fL/ ^9z_}7ຨQDm5Pk_-4Y`ag9Xo`6`bY7N?p$Fyy_#']<f3K:;gN|#/mZ%j~X_w&$#>ۛk,$K+k F_恞vNרX~2yǽ"sT4,W.fÂX䉓"D)SU"{&j e_[_rKբץgv5uuwb~k{_EF6vdS]9w׃/Rg"ksi!pichyڑ7't4sy)? mTq4$Lm}4hI˾dN,VJiur|"\M3&|>_.dűH(>flO9Vyw>neYDzIv''DId a'MPGmYlH,uJiV d9O>oڲrݺnj.'`G('uY'_ P+^5oH~focustimerhq-FocusTimer-8581be2/focus-timer.doap000066400000000000000000000021251520625676500217610ustar00rootroot00000000000000 Focus Timer Maintain focus by taking frequent breaks Vala Kamil Prusko focustimerhq-FocusTimer-8581be2/io.github.focustimerhq.FocusTimer.Devel.json000066400000000000000000000040411520625676500272660ustar00rootroot00000000000000{ "app-id": "io.github.focustimerhq.FocusTimer.Devel", "runtime": "org.gnome.Platform", "runtime-version": "50", "sdk": "org.gnome.Sdk", "command": "focus-timer", "tags": [ "devel" ], "separate-locales": false, "finish-args": [ "--device=dri", "--share=ipc", "--socket=fallback-x11", "--socket=wayland", "--socket=pulseaudio", "--system-talk-name=org.freedesktop.login1", "--system-talk-name=org.freedesktop.timedate1", "--talk-name=org.freedesktop.Flatpak", "--talk-name=org.freedesktop.Notifications", "--talk-name=org.gnome.Mutter.IdleMonitor", "--talk-name=org.gnome.ScreenSaver", "--talk-name=org.gnome.Shell", "--talk-name=org.gtk.vfs.*", "--talk-name=org.kde.StatusNotifierWatcher", "--talk-name=org.xfce.ScreenSaver", "--talk-name=io.github.focustimerhq.FocusTimer.ShellIntegration", "--filesystem=~/.local/share/gnome-pomodoro/:ro", "--filesystem=xdg-run/gvfsd", "--env=G_MESSAGES_DEBUG=focus-timer", "--env=G_ENABLE_DIAGNOSTIC=1" ], "cleanup": [ "/include", "/lib/girepository-1.0", "/lib/pkgconfig", "/share/gir-1.0", "/share/vala" ], "modules": [ { "name": "libpeas", "buildsystem": "meson", "builddir": true, "config-opts": [ "-Dgjs=false", "-Dlua51=false", "-Dpython3=false", "-Dintrospection=true", "-Dvapi=true" ], "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/GNOME/libpeas.git", "tag": "2.2.0" } ] }, { "name": "gom", "buildsystem": "meson", "builddir": true, "sources": [ { "type": "git", "url": "https://gitlab.gnome.org/GNOME/gom.git", "tag": "0.5.5" } ] }, { "name": "focus-timer", "buildsystem": "meson", "config-opts": [ "-Dprofile=development" ], "sources": [ { "type": "dir", "path": "." } ] } ] } focustimerhq-FocusTimer-8581be2/lint/000077500000000000000000000000001520625676500176255ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/lint/vala-lint.ini000066400000000000000000000007171520625676500222220ustar00rootroot00000000000000[Checks] block-opening-brace-space-before=off double-semicolon=error double-spaces=off ellipsis=error line-length=error naming-convention=error no-space=error note=warn space-before-paren=error use-of-tabs=error trailing-newlines=error trailing-whitespace=error unnecessary-string-template=error [Disabler] disable-by-inline-comments=true [line-length] max-line-length=120 ignore-comments=true [naming-convention] exceptions=UUID, [note] keywords=TODO,FIXME, focustimerhq-FocusTimer-8581be2/meson.build000066400000000000000000000135731520625676500210320ustar00rootroot00000000000000project( 'focus-timer', ['vala', 'c'], version: '1.1.2', meson_version: '>=0.59.0', default_options: [ 'warning_level=2', ], ) i18n = import('i18n') gnome = import ('gnome') glib_dep = dependency('glib-2.0', version: '>=2.50') gobject_dep = dependency('gobject-2.0', version: '>=2.50') gio_dep = dependency('gio-2.0', version: '>=2.50') gtk_dep = dependency('gtk4', version: '>=4.18') cairo_dep = dependency('cairo') graphene_dep = dependency('graphene-gobject-1.0') libadwaita_dep = dependency('libadwaita-1', version: '>= 1.7.0') introspection_dep = dependency('gobject-introspection-1.0', version: '>=0.10.1') peas_dep = dependency('libpeas-2', version: '>=2.0.0') gom_dep = dependency('gom-1.0', version: '>=0.5.0') gstreamer_dep = dependency('gstreamer-1.0', version: '>=1.0.10') gstreamer_controller_dep = dependency('gstreamer-controller-1.0') json_dep = dependency('json-glib-1.0', version: '>=1.6.2') sqlite_dep = dependency('sqlite3') pangocairo_dep = dependency('pangocairo') gtk_x11_dep = dependency('gtk4-x11', required: false) gtk_wayland_dep = dependency('gtk4-wayland', required: get_option('plugin_wayland')) wayland_client_dep = dependency('wayland-client', required: get_option('plugin_wayland')) wayland_scanner = find_program('wayland-scanner', required: get_option('plugin_wayland')) bash_completion_dep = dependency('bash-completion', required: false) cc = meson.get_compiler('c') valac = meson.get_compiler('vala') libm_dep = cc.find_library('m') posix_dep = valac.find_library('posix') check_headers = [ 'sys/types.h', 'unistd.h', 'langinfo.h', 'locale.h' ] foreach h : check_headers cc.has_header(h, required: true) endforeach add_project_arguments( [ '-DGETTEXT_PACKAGE="' + meson.project_name() + '"', '-DG_LOG_DOMAIN="' + meson.project_name() + '"', ], language: 'c', ) # Ignore warnings for C code generated by Vala. add_project_arguments( [ '-DVALA_STRICT_C', '-Wno-error', '-Wno-discarded-qualifiers', '-Wno-incompatible-pointer-types', '-Wno-cast-function-type', '-Wno-unused-parameter', '-Wno-unused-label', '-Wno-unused-but-set-variable', '-Wno-missing-field-initializers', '-Wno-unused-variable', '-Wno-unused-function', '-Wno-sign-compare', '-Wno-alloc-size', '-Wno-address', # TODO: Remove this after upgrading to valac 0.57+. # Vala doesn't generate proper headers for construct properties # of interface type. '-Wno-implicit-function-declaration', ], language: 'c', ) # Enable GNU-specific extensions to get access to additional locale-related # functionality beyond what's available in the standard POSIX. add_project_arguments('-D_GNU_SOURCE', language: 'c') add_project_arguments( [ '--vapidir', meson.current_source_dir(), '--vapidir', meson.current_source_dir() / 'vapi', '--pkg', 'config', ], language: 'vala', ) if gtk_x11_dep.found() add_project_arguments(['-D', 'HAVE_GDK_X11'], language: 'vala') endif if gtk_wayland_dep.found() add_project_arguments(['-D', 'HAVE_GDK_WAYLAND'], language: 'vala') endif if get_option('automation').enabled() add_project_arguments(['-D', 'ENABLE_AUTOMATION'], language: 'vala') endif if get_option('profile') == 'development' add_project_arguments(['--enable-checking'], language: 'vala') endif if valac.version().version_compare('>= 0.56.19') add_project_arguments(['-D', 'VALA_0_56_19'], language: 'vala') endif application_id = 'io.github.focustimerhq.FocusTimer' application_name = 'Focus Timer' package_name = meson.project_name() package_version = meson.project_version() package_website = 'https://github.com/focustimerhq/FocusTimer' package_issue_url = 'https://github.com/focustimerhq/FocusTimer/issues' package_support_url = 'https://github.com/focustimerhq/FocusTimer/discussions' package_donate_url = 'https://liberapay.com/kamilprusko' package_datadir = get_option('prefix') / get_option('datadir') / meson.project_name() package_localedir = get_option('prefix') / get_option('datadir') / 'locale' gschema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0' / 'schemas' gettext_package = package_name # Change app id for development builds if get_option('profile') == 'development' application_id = '@0@.Devel'.format(application_id) application_name = '@0@ (Development)'.format(application_name) endif enable_plugin_wayland = ( not get_option('plugin_wayland').disabled() and gtk_wayland_dep.found() and wayland_client_dep.found() and wayland_scanner.found() ) conf = configuration_data() conf.set_quoted( 'APPLICATION_ID', application_id, ) conf.set_quoted( 'PACKAGE_LOCALE_DIR', package_localedir, ) conf.set_quoted( 'PACKAGE_DATA_DIR', package_datadir, ) conf.set_quoted( 'PACKAGE_NAME', package_name, ) conf.set_quoted( 'PACKAGE_VERSION', package_version, ) conf.set_quoted( 'PACKAGE_WEBSITE', package_website, ) conf.set_quoted( 'PACKAGE_ISSUE_URL', package_issue_url, ) conf.set_quoted( 'PACKAGE_SUPPORT_URL', package_support_url, ) conf.set_quoted( 'PACKAGE_DONATE_URL', package_donate_url, ) conf.set_quoted( 'GETTEXT_PACKAGE', gettext_package, ) conf.set_quoted( 'GSETTINGS_SCHEMA_DIR', gschema_dir, ) conf.set( 'HAVE_ALTMON', cc.has_header_symbol('langinfo.h', 'ALTMON_1', args: '-D_GNU_SOURCE') ) configure_file( output: 'config.h', configuration: conf, ) # Include the config.h we just generated config_h_dir = include_directories('.') subdir('po') subdir('data') subdir('src') subdir('tests') summary( { 'freedesktop': true, 'portal': true, 'gnome': get_option('plugin_gnome').enabled(), 'kde': get_option('plugin_kde').enabled(), 'sni': get_option('plugin_sni').enabled(), 'wayland': enable_plugin_wayland, 'xfce': get_option('plugin_xfce').enabled(), }, section: 'Plugins', bool_yn: true, ) summary( { 'automation': get_option('automation').enabled(), }, section: 'Features', bool_yn: true, ) focustimerhq-FocusTimer-8581be2/meson_options.txt000066400000000000000000000015111520625676500223120ustar00rootroot00000000000000option( 'profile', type: 'combo', choices: [ 'default', 'development', ], value: 'default', description: 'The build profile for Focus Timer. One of "default" or "development".' ) option( 'automation', type: 'feature', value: 'enabled', description: 'Enable Automation panel.' ) option( 'plugin_gnome', type: 'feature', value: 'enabled', description: 'Enable GNOME integration.' ) option( 'plugin_kde', type: 'feature', value: 'enabled', description: 'Enable KDE integration.' ) option( 'plugin_sni', type: 'feature', value: 'enabled', description: 'Enable indicator.' ) option( 'plugin_wayland', type: 'feature', value: 'auto', description: 'Enable Wayland integration.' ) option( 'plugin_xfce', type: 'feature', value: 'enabled', description: 'Enable XFCE integration.' ) focustimerhq-FocusTimer-8581be2/po/000077500000000000000000000000001520625676500172755ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/po/LINGUAS000066400000000000000000000001711520625676500203210ustar00rootroot00000000000000# ca # cs # de # el # eo # es # fi # fr # he # hr id # it ka # kk # ko lt # nb # nl pl # pt_BR # ru sv # ta # te # zh_CN focustimerhq-FocusTimer-8581be2/po/POTFILES000066400000000000000000000173631520625676500204570ustar00rootroot00000000000000data/io.github.focustimerhq.FocusTimer.desktop.in.in data/io.github.focustimerhq.FocusTimer.metainfo.xml.in src/application.vala src/core/action-list-model.vala src/core/action-manager.vala src/core/action.vala src/core/background-manager.vala src/core/command.vala src/core/context.vala src/core/cycle.vala src/core/database.vala src/core/date-utils.vala src/core/event-bus.vala src/core/event-producer.vala src/core/event.vala src/core/expression-parser.vala src/core/expression.vala src/core/gap-entry.vala src/core/gap.vala src/core/idle-monitor.vala src/core/indicator.vala src/core/job-queue.vala src/core/keyboard-manager.vala src/core/locale.vala src/core/lock-screen.vala src/core/logger.vala src/core/matrix.vala src/core/notification-backend.vala src/core/notification-manager.vala src/core/notification.vala src/core/priority.vala src/core/provided-object.vala src/core/provider-set.vala src/core/provider.vala src/core/scheduler.vala src/core/screen-overlay-manager.vala src/core/screen-saver.vala src/core/session-entry.vala src/core/session-manager-action-group.vala src/core/session-manager.vala src/core/session.vala src/core/settings.vala src/core/sleep-monitor.vala src/core/sound-manager.vala src/core/sound-player.vala src/core/sounds.vala src/core/state.vala src/core/stats-entry.vala src/core/stats-manager.vala src/core/time-block-entry.vala src/core/time-block.vala src/core/timer-action-group.vala src/core/timer.vala src/core/timestamp.vala src/core/timezone-entry.vala src/core/timezone-history.vala src/core/timezone-monitor.vala src/core/utils.vala src/core/variables.vala src/dbus-services.vala src/main.vala src/plugins/freedesktop/freedesktop.vala src/plugins/freedesktop/interfaces.vala src/plugins/freedesktop/lock-screen-provider.vala src/plugins/freedesktop/notification-backend-provider.vala src/plugins/freedesktop/sleep-monitor-provider.vala src/plugins/freedesktop/timezone-monitor-provider.vala src/plugins/gnome/application-extension.vala src/plugins/gnome/gnome.vala src/plugins/gnome/idle-monitor-provider.vala src/plugins/gnome/indicator-provider.vala src/plugins/gnome/install-extension-dialog.ui src/plugins/gnome/install-extension-dialog.vala src/plugins/gnome/interfaces.vala src/plugins/gnome/preferences-window-extension.vala src/plugins/gnome/screen-overlay-provider.vala src/plugins/gnome/screen-saver-provider.vala src/plugins/gnome/shell-extension-settings.vala src/plugins/gnome/shell-extension.vala src/plugins/gnome/window-extension.vala src/plugins/kde/kde.vala src/plugins/kde/preferences-window-extension.vala src/plugins/portal/background-provider.vala src/plugins/portal/global-shortcuts-provider.vala src/plugins/portal/interfaces.vala src/plugins/portal/notification-backend-provider.vala src/plugins/portal/portal.vala src/plugins/portal/request.vala src/plugins/sni/capabilities.vala src/plugins/sni/dbus-services.vala src/plugins/sni/indicator-action-group.vala src/plugins/sni/indicator-provider.vala src/plugins/sni/interfaces.vala src/plugins/sni/menu-item.vala src/plugins/sni/preferences-window-extension.vala src/plugins/sni/sni.vala src/plugins/wayland/idle-monitor-provider.vala src/plugins/wayland/wayland.vala src/plugins/xfce/interfaces.vala src/plugins/xfce/screen-saver-provider.vala src/plugins/xfce/xfce.vala src/ui/interfaces.vala src/ui/log/log-window.ui src/ui/log/log-window.vala src/ui/log/widgets/time-label.vala src/ui/main/dialogs/about-dialog.vala src/ui/main/stats/charts/bar-chart.vala src/ui/main/stats/charts/bubble-chart.ui src/ui/main/stats/charts/bubble-chart.vala src/ui/main/stats/charts/canvas-layout.vala src/ui/main/stats/charts/canvas.vala src/ui/main/stats/charts/chart-axis.vala src/ui/main/stats/charts/chart-contents.vala src/ui/main/stats/charts/chart-grid.vala src/ui/main/stats/charts/chart.ui src/ui/main/stats/charts/chart.vala src/ui/main/stats/stats-day-page.ui src/ui/main/stats/stats-day-page.vala src/ui/main/stats/stats-month-page.ui src/ui/main/stats/stats-month-page.vala src/ui/main/stats/stats-page.vala src/ui/main/stats/stats-view.ui src/ui/main/stats/stats-view.vala src/ui/main/stats/stats-week-page.ui src/ui/main/stats/stats-week-page.vala src/ui/main/stats/widgets/day-chooser.ui src/ui/main/stats/widgets/day-chooser.vala src/ui/main/stats/widgets/month-chooser.ui src/ui/main/stats/widgets/month-chooser.vala src/ui/main/stats/widgets/stats-card.ui src/ui/main/stats/widgets/stats-card.vala src/ui/main/stats/widgets/stats-date-popover.ui src/ui/main/stats/widgets/stats-date-popover.vala src/ui/main/stats/widgets/week-chooser.ui src/ui/main/stats/widgets/week-chooser.vala src/ui/main/timer/compact-timer-view.ui src/ui/main/timer/compact-timer-view.vala src/ui/main/timer/menus.ui src/ui/main/timer/timer-view.ui src/ui/main/timer/timer-view.vala src/ui/main/timer/widgets/session-progress-bar.vala src/ui/main/timer/widgets/timer-control-buttons.ui src/ui/main/timer/widgets/timer-control-buttons.vala src/ui/main/timer/widgets/timer-label.ui src/ui/main/timer/widgets/timer-label.vala src/ui/main/timer/widgets/timer-progress-bar.vala src/ui/main/widgets/size-stack.vala src/ui/main/window.ui src/ui/main/window.vala src/ui/overlays/lightbox.ui src/ui/overlays/lightbox.vala src/ui/overlays/screen-overlay.ui src/ui/overlays/screen-overlay.vala src/ui/preferences/appearance/preferences-panel-appearance.ui src/ui/preferences/appearance/preferences-panel-appearance.vala src/ui/preferences/automation/action/action-edit-window.ui src/ui/preferences/automation/action/action-edit-window.vala src/ui/preferences/automation/action/action-listboxrow.ui src/ui/preferences/automation/action/action-listboxrow.vala src/ui/preferences/automation/action/command-entryrow.ui src/ui/preferences/automation/action/command-entryrow.vala src/ui/preferences/automation/action/condition-group-widget.ui src/ui/preferences/automation/action/condition-group-widget.vala src/ui/preferences/automation/action/condition-widget.ui src/ui/preferences/automation/action/condition-widget.vala src/ui/preferences/automation/action/variable-popover.ui src/ui/preferences/automation/action/variable-popover.vala src/ui/preferences/automation/preferences-panel-automation.ui src/ui/preferences/automation/preferences-panel-automation.vala src/ui/preferences/integrations/preferences-panel-integrations.ui src/ui/preferences/integrations/preferences-panel-integrations.vala src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.vala src/ui/preferences/keyboard-shortcuts/accelerator-row.ui src/ui/preferences/keyboard-shortcuts/accelerator-row.vala src/ui/preferences/keyboard-shortcuts/accelerator.vala src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.vala src/ui/preferences/notifications/preferences-panel-notifications.ui src/ui/preferences/notifications/preferences-panel-notifications.vala src/ui/preferences/preferences-window.ui src/ui/preferences/preferences-window.vala src/ui/preferences/sounds/preferences-panel-sounds.ui src/ui/preferences/sounds/preferences-panel-sounds.vala src/ui/preferences/sounds/sound-chooser-window.ui src/ui/preferences/sounds/sound-chooser-window.vala src/ui/preferences/sounds/volume-slider.ui src/ui/preferences/sounds/volume-slider.vala src/ui/preferences/timer/preferences-panel-timer.ui src/ui/preferences/timer/preferences-panel-timer.vala src/ui/preferences/timer/widgets/log-scale-row.ui src/ui/preferences/timer/widgets/log-scale-row.vala src/ui/preferences/timer/widgets/log-scale.vala src/ui/preferences/widgets/preferences-sidebar.vala src/ui/screen-overlay-provider.vala src/ui/screen-saver-provider.vala src/ui/utils.vala src/ui/widgets/checkmark.vala src/ui/widgets/gizmo.vala src/ui/widgets/monospace-label.vala src/ui/widgets/sidebar-row.ui src/ui/widgets/sidebar-row.vala focustimerhq-FocusTimer-8581be2/po/ca.po000066400000000000000000002024441520625676500202260ustar00rootroot00000000000000# Catalan translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Ecron , 2014. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 15:33+0100\n" "Last-Translator: Antonio Vicién Faure \n" "Language-Team: Catalan\n" "Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Un temporitzador de productivitat que us ajuda a treballar de manera més " "eficaç dividint el temps en sessions de treball concentrat seguides de " "descansos curts. Treballeu durant 25 minuts i després feu un descans de 5 " "minuts per mantenir la concentració i evitar l'esgotament." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Característiques principals:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Durada personalitzable de les sessions de treball i dels descansos" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Superposició de pantalla durant els descansos" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Temporitzador" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Estadístiques diàries" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Estadístiques mensuals" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferències" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Superposició de pantalla" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Inicia o Atura" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Inicia, Pausa o Reprèn" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Inicia el Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Inicia" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Atura" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pausa" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 #, fuzzy msgid "Resume" msgstr "Reprèn" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Omet" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Rebobina" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 #, fuzzy msgid "Extend current pomodoro or break" msgstr "Amplia el pomodoro actual o el descans" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Restableix" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Mostra les preferències" #: src/application.vala:203 msgid "Quit application" msgstr "Surt de l'aplicació" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Imprimeix la informació de la versió i surt" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Porta al focus" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "queden %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "L'acció personalitzada «%s» ha fallat" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "S'ha esgotat el temps d'espera" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "No s'ha pogut executar l'ordre" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "L'ordre està buida" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Marca de cometes no tancada" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Ordre no vàlida" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Variable desconeguda «%s»" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Format desconegut «%s»" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "No s'ha trobat el programa «%s»" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Accions" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Compte enrere" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sessió" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Altres" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "S'ha iniciat el temporitzador." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "S'ha aturat el temporitzador manualment." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "El compte enrere s'ha pausat manualment. No es dispara en bloquejar la " "pantalla o en suspendre el sistema." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "El compte enrere s'ha reprès manualment." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "" "S'ha saltat al següent bloc de temps abans que el compte enrere hagi " "finalitzat." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "S'ha utilitzat l'acció de rebobinar. Afegeix una pausa en el passat." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "S'ha netejat la sessió manualment." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Finalitzat" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "El compte enrere ha finalitzat. Si s'està esperant confirmació, la durada " "del bloc de temps encara es pot alterar." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Canviat" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Es dispara amb qualsevol canvi relacionat amb el compte enrere." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Confirma l'avanç" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Cal una confirmació manual per iniciar el següent bloc de temps." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Avançat" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "S'ha passat o saltat al següent bloc de temps." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Estat canviat" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" "S'ha passat al següent bloc de temps o s'ha canviat l'etiqueta d'un descans." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Replanificat" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Es dispara quan els blocs de temps planificats han canviat." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Expirat" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Es dispara quan la sessió està a punt de restablir-se per inactivitat." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 #, fuzzy msgid "Take a break" msgstr "Fes un descans" #: src/core/notification-manager.vala:355 #, fuzzy msgid "Take a short break" msgstr "Fes un descans curt" #: src/core/notification-manager.vala:359 #, fuzzy msgid "Take a long break" msgstr "Fes un descans llarg" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "El Pomodoro està a punt de finalitzar" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Fes un descans" #: src/core/notification-manager.vala:425 #, fuzzy msgid "Break is about to end" msgstr "El descans està a punt de finalitzar" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minut" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Prepara't…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "El Pomodoro ha acabat!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "El descans ha acabat!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Confirma l'inici d'un Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Confirma l'inici d'un descans…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Confirma l'inici d'un descans curt…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Confirma l'inici d'un descans llarg…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Omet el descans" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "No s'ha pogut inicialitzar la reproducció" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "No s'ha trobat el fitxer" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Tipus de fitxer no admès" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Aturat" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Descans" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Descans curt" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Descans llarg" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u hores" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minuts" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segon" msgstr[1] "%u segons" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "L'hora exacta de l'esdeveniment actual." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "La fase actual del cicle Pomodoro. Valors possibles: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Estat del bloc de temps actual. Valors possibles: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Un indicador que assenyala si el compte enrere ha començat." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Un indicador que assenyala si el compte enrere està pausat." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Un indicador que assenyala si el compte enrere ha acabat." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "" "Un indicador que assenyala si el temporitzador està comptant activament." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Durada del compte enrere actual." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Discrepància entre el temps transcorregut i el temps real passat." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "La quantitat de temps dedicat al compte enrere." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "La quantitat de temps restant abans que acabi el compte enrere." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Hora en què s'ha iniciat el compte enrere." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Extensió del GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Obteniu la millor experiència!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Habiliteu l'extensió del GNOME Shell per a una integració perfecta " "amb l'escriptori" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Sempre a l'abast" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "" "Controleu el temporitzador directament des de la barra superior sense obrir " "l'aplicació" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Menys distraccions" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Recordatoris de descans refinats" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Superposició elegant a pantalla completa que fa que els descansos siguin una " "experiència més agradable" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Esteu preparat per provar-ho?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Instal·la l'extensió" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "Ara _no" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Alguna cosa ha anat malament" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Copia al porta-retalls" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Torna-ho a provar" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Avorta" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "S'ha esgotat el temps d'espera" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "No es permet instal·lar extensions" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "No s'ha pogut baixar l'extensió" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Superposició de pantalla" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Escriptori" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Extensió del GNOME Shell disponible" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Saber-ne més" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "No utilitzat" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Finalitzat!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Estadístiques" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Surt" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Registre" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Registre buit" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Les entrades apareixeran aquí quan inicieu el temporitzador." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Context" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Ordre" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Sortida" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Error" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Codi de sortida:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Temps d'execució:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Antonio Vicién Faure " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Donatius" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Descansos" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Interrupcions" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Proporció de descansos" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dia" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Setmana" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mes" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Encara no hi ha res per veure" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Completeu uns quants Pomodoros per omplir-ho!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "S'ha omès %u dia" msgstr[1] "S'han omès %u dies" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "S'ha omès %u setmana" msgstr[1] "S'han omès %u setmanes" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "S'ha omès %u mes" msgstr[1] "S'han omès %u mesos" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Avui" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Ahir" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Aquesta setmana" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Setmana %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Setmana %u de %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Descans _curt" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Descans _llarg" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Descans" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Obre la superposició de pantalla" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "La sessió ha expirat" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Descans llarg en %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Rebobina un minut" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "Vista _compacta" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferències" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Quant a" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Surt" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Menú principal" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Voleu mantenir el temporitzador en marxa?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Podeu mantenir-lo en marxa en segon pla; les notificacions i les dreceres de " "teclat continuaran funcionant." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Executa en segon pla" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "És hora de fer un descans" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Finestra principal" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Prefereix el tema fosc" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Prefereix la vista compacta" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Iniciat" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "En pausa" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Edita l'acció personalitzada" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Cancel·la" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Desa" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nom" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Activador" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Esdeveniment" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Executa una ordre després d'un esdeveniment." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Condició" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Assegura l'execució d'una segona ordre un cop la condició ja no es compleixi." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Esdeveniments" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Afegeix un _esdeveniment" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtra" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filtre" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Ordre de l'intèrpret" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Ordres" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Ordre de condició complerta" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Ordre de condició no complerta" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Directori de treball" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Usa un sub-intèrpret" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Executa el programa des d'un sub-intèrpret com ara sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Passa dades d'entrada" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "En lloc de passar variables, podeu processar un objecte JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Espera que finalitzi" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Bloqueja l'execució d'altres ordres fins que l'ordre hagi acabat." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Suprimeix l'acció" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Encara no s'ha especificat cap esdeveniment." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Afegeix una acció personalitzada" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Afegeix" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Seleccioneu el directori de treball" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Selecciona" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Acció sense títol" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Afegeix una condició" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Afegeix un grup" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "I" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "O" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "És" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "No és" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "És igual a" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Major que" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Menor que" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Sí" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "No" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuts" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Segons" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Hores" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Selecciona un camp…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Estat" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "En execució" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Durada" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Insereix una variable" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Registre" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Mostra el registre d'execució" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Executeu ordres de l'intèrpret automàticament en esdeveniments o condicions " "del temporitzador. Saber-ne més." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Estableix la drecera" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Estableix" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Inhabilitat" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Premeu Esc per cancel·lar o Retrocés per inhabilitar la " "drecera de teclat" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Les dreceres globals us permeten controlar l'aplicació encara que no estigui " "a la pantalla. Funcionen mentre l'aplicació s'executi en segon pla." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Obriu la configuració de l'aplicació per editar les dreceres globals" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Edita" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Introduïu una drecera nova per iniciar o aturar el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Introduïu una drecera nova per iniciar/pausar/reprendre el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Introduïu una drecera nova per iniciar el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Introduïu una drecera nova per aturar el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Introduïu una drecera nova per pausar el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Introduïu una drecera nova per reprendre el temporitzador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Introduïu una drecera nova per ometre" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Rebobina un minut" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Introduïu una drecera nova per rebobinar" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Introduïu una drecera nova per portar la finestra al focus" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Anuncis" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "El temps s'acaba" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Notifica quan el Pomodoro o el descans estiguin a punt de finalitzar." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Una notificació a pantalla completa destinada a forçar el descans." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Retard de bloqueig" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Període d'inactivitat per bloquejar la pantalla." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Retard de reobertura" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Període d'inactivitat per tornar a obrir la superposició després d'haver " "estat tancada." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Mai" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notificacions" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Sons" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Aparença" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Dreceres de teclat" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatització" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Els sons estan inhabilitats" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Sons d'alerta" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "So de Pomodoro finalitzat" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "So de descans finalitzat" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "So de fons" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Campana" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Campana potent" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tic-tac del rellotge" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Cap" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volum:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Trieu un so personalitzat" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Durada del Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Durada del descans curt" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Durada del descans llarg" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Nombre de cicles" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Comportament" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pausa en bloquejar la pantalla" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Confirma l'inici d'un descans" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Confirma l'inici d'un Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Una sessió única durarà %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "Es dedicarà el %u%% del temps als descansos." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Voleu aplicar els canvis al Pomodoro actual?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Voleu aplicar els canvis al descans actual?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Aplica" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Barra lateral" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Utilitat de gestió del temps" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Mantingueu la concentració fent descansos freqüents" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Notificacions visuals i d'àudio" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Seguiment del temps i estadístiques" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integració amb l'escriptori GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "" #~ "Executeu ordres personalitzades després d'un Pomodoro o d'un descans" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Temporitzador compacte" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "S'ha afegit la traducció al tàmil (gràcies @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "S'ha afegit la traducció a l'hebreu (gràcies @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Compatibilitat amb GNOME Shell 49 (gràcies @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Traducció a l'alemany actualitzada (gràcies @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Compatibilitat amb GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Divideix el temps passat a través de la mitjanit" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "S'ha afegit la traducció al telugu (gràcies @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Compatibilitat amb GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Permet tancar la superposició de pantalla mitjançant un gest quan s'està " #~ "reproduint un vídeo" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "S'ha afegit la traducció al georgià (gràcies @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Traduccions ajustades a l'appdata (gràcies @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "" #~ "Correcció de la notificació persistent després d'ampliar el Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Correccions per a GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "S'elimina la compatibilitat amb GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Resum dels canvis a gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Compatibilitat amb GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "S'ha ajustat l'script de construcció a meson 0.59.0 (gràcies @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Deixeu que Pomodoro gestioni les notificacions del sistema mentre " #~ "el temporitzador està en marxa" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 segons" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 segons" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minut" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minuts" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minuts" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minuts" #~ msgid "Timer Ticking" #~ msgstr "Tic-tac del temporitzador" #, fuzzy #~ msgid "Birds" #~ msgstr "Ocells" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "temporitzador;pomodoro;temps;" #~ msgid "Start/Stop" #~ msgstr "Inicia/Atura" #~ msgid "Pause/Resume" #~ msgstr "Pausa/Reprèn" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Salta a un pomodoro o a un descans" #~ msgid "Reset current session" #~ msgstr "Restableix la sessió actual" #~ msgid "Run as background service" #~ msgstr "Executa com a servei en segon terme" #~ msgid "About Pomodoro" #~ msgstr "Quant a Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Una senzilla utilitat de gestió del temps" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Atura" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indicador del GNOME Shell" #, fuzzy #~ msgid "_Install" #~ msgstr "Instaŀla" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "No s'ha pogut habilitar l'extensió" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Durada del descans llarg" #~ msgid "A time management utility for GNOME" #~ msgstr "Una utilitat de gestió del temps per al GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Una utilitat del GNOME que ajuda a gestionar el temps seguint la Tècnica " #~ "Pomodoro. Pretén millorar la productivitat i la concentració fent petits " #~ "descansos cada 25 minuts de treball." #~ msgid "Timer window" #~ msgstr "Finestra del temporitzador" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indicador del GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indicador del GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indicador del GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indicador del GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Temporitzador" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Drecera de teclat per a commutar el temporitzador. Teclegeu una nova " #~ "drecera per canviar-la." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoros fins a un descans llarg" #~ msgid "Keyboard shortcut" #~ msgstr "Drecera de teclat" #~ msgid "Screen notifications" #~ msgstr "Notificacions en pantalla" #~ msgid "Wait for activity after a break" #~ msgstr "Espera que hi hagi activitat després d'un descans" #~ msgid "Plugins…" #~ msgstr "Connectors…" #~ msgid "Plugins" #~ msgstr "Connectors" #~ msgid "Back" #~ msgstr "Enrere" #~ msgid "Complete a few sessions" #~ msgstr "Completeu algunes sessions" #~ msgid "Previous (Alt+Left)" #~ msgstr "Anterior (Alt+Esquerra)" #~ msgid "Next (Alt+Right)" #~ msgstr "Següent (Alt+Dreta)" #~ msgid "Complete" #~ msgstr "Completa" #~ msgid "Enable" #~ msgstr "Habilita" #~ msgid "Add" #~ msgstr "Afegeix" #~ msgid "Remove" #~ msgstr "Elimina" #~ msgid "Elapsed Time" #~ msgstr "Temps transcorregut" #~ msgid "Pause Timer" #~ msgstr "Pausa el temporitzador" #~ msgid "Pause break" #~ msgstr "Pausa el descans" #~ msgid "Pause Pomodoro" #~ msgstr "Pausa el Pomodoro" #~ msgid "Resume break" #~ msgstr "Reprendre" #~ msgid "Resume Pomodoro" #~ msgstr "Reprendre" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "Falta %d minut" #~ msgstr[1] "Falten %d minuts" #~ msgid "Report issue" #~ msgstr "Notifica un problema" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "El servei %s ha fallat" #~ msgid "Woodland Birds" #~ msgstr "Ocells de bosc" #~ msgid "End of Break Sound" #~ msgstr "So del final del descans" #~ msgid "Start of Break Sound" #~ msgstr "So de l'inici del descans" #~ msgid "Off" #~ msgstr "Desactivat" #~ msgid "Ticking sound" #~ msgstr "So de tic-tac" #~ msgid "Start of break sound" #~ msgstr "So de l'inici del descans" #~ msgid "End of break sound" #~ msgstr "So del final del descans" #~ msgid "Focus on your task." #~ msgstr "Centreu-vos en la feina." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Teniu %d minut" #~ msgstr[1] "Teniu %d minuts" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Teniu %d segon" #~ msgstr[1] "Teniu %d segons" #~ msgid "Take a longer break" #~ msgstr "Feu un descans llarg" #~ msgid "Lengthen it" #~ msgstr "Allarga'l" #~ msgid "Shorten it" #~ msgstr "Escurça'l" #~ msgid "Start pomodoro" #~ msgstr "Inicia el Pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Utilitzar \"%s\" com a drecera interferirà l'escriptura. Proveu d'afegir " #~ "una altra tecla com Control, Alt o Majúscules." #~ msgid "Available" #~ msgstr "Disponible" #~ msgid "Busy" #~ msgstr "Ocupat" #~ msgid "Idle" #~ msgstr "Absent" #~ msgid "Invisible" #~ msgstr "Invisible" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Estadístiques" #~ msgid "It seems to be uninstalled" #~ msgstr "Sembla que ha estat desinstal·lada" #~ msgid "Extension is out of date" #~ msgstr "L'extensió està desactualitzada" #~ msgid "Upgrade" #~ msgstr "Actualitza" #~ msgid "A new pomodoro is starting" #~ msgstr "La pausa ha finalitzat, comença un nou pomodoro" #~ msgid "Hey, you're missing out on a break" #~ msgstr "No esteu aprofitant el descans" #~ msgid "Could not run pomodoro" #~ msgstr "Pomodoro no s'ha pogut executar" #~ msgid "Looks like gnome-pomodoro is not installed" #~ msgstr "Sembla que gnome-pomodoro no està instal·lat" #~ msgid "" #~ "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; either version 3 of the License, or (at your " #~ "option) any later version." #~ msgstr "" #~ "Aquest programa és programari lliure: podeu modificar-lo o redistribuir-" #~ "lo atenent als termes de la GNU General Public License tal com va ser " #~ "publicada per la Free Software Foundation; sigui tant la versió 3 de la " #~ "Llicència com (a la vostra elecció) qualsevol versió posterior." #~ msgid "Remind to take a break" #~ msgstr "Recorda'm fer una pausa" #~ msgid "Select sound for pomodoro start" #~ msgstr "Trieu un so per iniciar un pomodoro" #~ msgid "Presence" #~ msgstr "Presència" #~ msgid "Postpone pomodoro when idle" #~ msgstr "Posposa el pomodoro en estar inactiu" #~ msgid "Status during pomodoro" #~ msgstr "Estat durant el pomodoro" #~ msgid "" #~ "System notifications including chat messages won't show up during " #~ "pomodoro." #~ msgstr "" #~ "Durant el pomodoro no es mostraran les notificacions del sistema, " #~ "inclosos els missatges de xat." #~ msgid "" #~ "System notifications including chat messages won't show up during break." #~ msgstr "" #~ "Durant el descans no es mostraran les notificacions del sistema, inclosos " #~ "els missatges de xat." #~ msgid "System notifications including chat messages won't show up." #~ msgstr "" #~ "No es mostraran les notificacions del sistema, inclosos els missatges de " #~ "xat." #~ msgid "OK" #~ msgstr "D'acord" #~ msgid "" #~ "The shortcut \"%s\" cannot be used because it will become impossible to " #~ "type using this key.\n" #~ "Please try with a key such as Control, Alt or Shift at the same time." #~ msgstr "" #~ "No es pot usar la drecera \"%s\" perquè seria impossible escriure al fer " #~ "servir aquesta tecla.\n" #~ "Utilitzeu a més una tecla com Control, Alt o Majús." #~ msgid "_No sound" #~ msgstr "_Sense so" #~ msgid "_Open" #~ msgstr "_Obre" #~ msgid "All files" #~ msgstr "Tots els fitxers" #~ msgid "Supported audio files" #~ msgstr "Fitxers d'àudio compatibles" #~ msgid "Manage your time and tasks" #~ msgstr "Gestioneu el vostre temps i tasques" #~ msgid "time;timer;tasks;manage;organize;" #~ msgstr "time;timer;tasks;manage;organize;" #~ msgid "Reset Counts and Timer" #~ msgstr "Reinicia el comptador i el temporitzador" #~ msgid "Click to reset session counts to zero" #~ msgstr "Premeu per reiniciar el comptador de la sessió a zero" #~ msgid "Away From Desk" #~ msgstr "Absent a l'escriptori" #~ msgid "Set optimal settings for doing paperwork" #~ msgstr "Paràmetres òptims per fer paperassa" #~ msgid "Control Presence Status" #~ msgstr "Control de l'estat de presència" #~ msgid "Show Dialog Messages" #~ msgstr "Mostra missatges de diàleg" #~ msgid "Show a dialog message at the end of pomodoro session" #~ msgstr "Mostra un missatge de diàleg al final de la sessió de pomodoro" #~ msgid "Play a sound at start of pomodoro session" #~ msgstr "Reprodueix un so al començar la sessió de pomodoro" #~ msgid "%d Completed Session" #~ msgid_plural "%d Completed Sessions" #~ msgstr[0] "%d sessió completada" #~ msgstr[1] "%d sessions completades" #~ msgid "Hide" #~ msgstr "Oculta" #~ msgid "Timer toggle key" #~ msgstr "Tecla de commutació del temporitzador" #~ msgid "Time in seconds you are supposed to be working." #~ msgstr "Temps en segons que se suposa que són de treball." #~ msgid "Time in seconds you are supposed to have a short break." #~ msgstr "Temps en segons que se suposa que són de pausa curta." #~ msgid "Long pause duration" #~ msgstr "Durada de la pausa llarga" #~ msgid "Time in seconds you are supposed to have a longer break." #~ msgstr "Temps en segons que se suposa que són de pausa llarga." #~ msgid "Whether to show a notification dialog when pause starts." #~ msgstr "Si es mostra un diàleg de notificació quan s'inicia una pausa." #~ msgid "Disable flexible breaks" #~ msgstr "Inhabilita les pauses flexibles" #~ msgid "Whether you are not using a computer to work." #~ msgstr "Si no s'està fent servir un ordinador per treballar." #~ msgid "Change user presence status to busy" #~ msgstr "Canvia l'estat de presència de l'usuari a ocupat" #~ msgid "Whether to change user and IM presence to busy." #~ msgstr "Si es canvia l'usuari i la presència de MI a ocupat." #~ msgid "Whether to play a sound to notify of events." #~ msgstr "Si es reprodueix un so per notificar els esdeveniments." #~ msgid "Notification sound file" #~ msgstr "Fitxer de so de la notificació" #~ msgid "Restore timer state" #~ msgstr "Restaura l'estat del temporitzador" #~ msgid "Whether to restore state on startup." #~ msgstr "Si es restaura l'estat a l'inici." #~ msgid "Number of completed sessions since long break" #~ msgstr "Nombre de sessions completades després d'una pausa llarga" #~ msgid "Saved timer state" #~ msgstr "Estat del temporitzador desat" #~ msgid "Time of saved state" #~ msgstr "Hora de l'estat desat" focustimerhq-FocusTimer-8581be2/po/cs.po000066400000000000000000001542361520625676500202550ustar00rootroot00000000000000# Czech translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Jakub Veverka, 2012. # Petr Hložek , 2023. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 15:38+0100\n" "Last-Translator: Petr Hložek \n" "Language-Team: Czech\n" "Language: cs\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Časovač produktivity, který vám pomůže pracovat efektivněji rozdělením času " "na soustředěné pracovní bloky následované krátkými přestávkami. Pracujte 25 " "minut a poté si dejte 5 minut pauzu pro udržení koncentrace a prevenci " "vyhoření." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Klíčové vlastnosti:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Nastavitelná délka práce a přestávek" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Překrytí obrazovky během přestávek" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Časovač" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Denní statistiky" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Měsíční statistiky" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Předvolby" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Překrytí obrazovky" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Spustit nebo zastavit" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Spustit, pozastavit nebo pokračovat" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Začít pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Start" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stop" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pozastavit" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Pokračovat" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Přeskočit" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Přetočit" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Prodloužit aktuální pomodoro nebo přestávku" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Resetovat" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Zobrazit předvolby" #: src/application.vala:203 msgid "Quit application" msgstr "Ukončit aplikaci" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Zobrazit informace o verzi a ukončit" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Přenést do popředí" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "Zbývá %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Vlastní akce \"%s\" selhala" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Časový limit vypršel" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Nepodařilo se spustit příkaz" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Příkaz je prázdný" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Neuzavřené uvozovky" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Neplatný příkaz" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Neznámá proměnná \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Neznámý formát \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Program \"%s\" nebyl nalezen" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Akce" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Odpočet" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Relace" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Ostatní" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Časovač spuštěn." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Časovač ručně zastaven." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Odpočet byl ručně pozastaven. Nespouští se při uzamčení obrazovky nebo " "uspání systému." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Odpočet byl ručně obnoven." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Přeskočeno na další blok před dokončením odpočtu." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Byla použita akce přetočení. Přidá pauzu do minulosti." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Relace byla ručně vymazána." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Dokončeno" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Odpočet skončil. Pokud se čeká na potvrzení, délka bloku může být ještě " "upravena." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Změněno" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Spuštěno při jakékoli změně související s odpočtem." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Potvrdit postun" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Pro spuštění dalšího bloku je vyžadováno ruční potvrzení." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Pokročilý" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Proběhl přechod nebo přeskok na další časový blok." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Stav změněn" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Proběhl přechod na další blok nebo došlo k přejmenování přestávky." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Přeplánováno" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Spuštěno, když se změnily naplánované časové bloky." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Vypršelo" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Spuštěno, když má být relace resetována kvůli nečinnosti." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Dejte si pauzu" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Udělejte si krátkou přestávku" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Dejte si dlouhou pauzu" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro brzy skončí" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Dejte si pauzu" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Přestávka se blíží ke konci" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuta" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Připravit se…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Pomodoro skončilo!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Přestávka skončila!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Potvrdit začátek pomodora…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Potvrdit začátek přestávky…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Potvrdit začátek krátké přestávky…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Potvrdit začátek dlouhé přestávky…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Přeskočit přestávku" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Selhala inicializace přehrávání" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Soubor nenalezen" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Typ souboru není podporován" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Zastaveno" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Přestávka" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Krátká přestávka" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Dlouhá přestávka" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hodina" msgstr[1] "%u hodiny" msgstr[2] "%u hodin" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minuty" msgstr[2] "%u minut" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekundy" msgstr[2] "%u sekund" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Přesný čas aktuální události." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Aktuální fáze cyklu Pomodoro. Možné hodnoty: stopped, pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status aktuálního bloku. Možné hodnoty: scheduled, in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Příznak indikující, zda odpočet začal." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Příznak indikující, zda je odpočet pozastaven." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Příznak indikující, zda odpočet skončil." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Příznak indikující, zda časovač aktivně odpočítává." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Délka aktuálního odpočtu." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Rozdíl mezi uplynulým časem a skutečně uběhlým časem." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Množství času stráveného odpočtem." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Množství času zbývajícího do konce odpočtu." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Čas, kdy odpočet začal." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Rozšíření GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Získejte ten nejlepší zážitek!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Povolit rozšíření GNOME Shell pro plynulou integraci s plochou" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Vždy na dosah" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Ovládejte časovač přímo z horní lišty bez otevírání aplikace" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Méně rušení" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Vylepšená připomenutí přestávek" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Elegantní celoobrazovkové překrytí, které zpříjemní dělání přestávek" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Chcete to zkusit?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Instalovat rozšíření" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Nyní ne" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Něco se pokazilo" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Kopírovat do schránky" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Zkusit znovu" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Zrušit" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Časový limit vypršel" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Instalace rozšíření není povolena" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Nepodařilo se stáhnout rozšíření" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Překrytí obrazovky" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Rozšíření GNOME Shell je k dispozici" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Dozvědět se více" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Nepoužito" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Hotovo!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistiky" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Ukončit" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Záznam" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Prázdný záznam" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Záznamy se zde objeví, jakmile spustíte časovač." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Kontext" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Příkaz" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Výstup" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Chyba" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Návratový kód:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Čas spuštění:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Petr Hložek" #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Přispět" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Přestávky" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Přerušení" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Poměr přestávek" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Den" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Týden" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Měsíc" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Zatím zde není nic k vidění" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Dokončete pár Pomodor, aby se to tu zaplnilo!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Přeskočen %u den" msgstr[1] "Přeskočeny %u dny" msgstr[2] "Přeskočeno %u dní" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Přeskočen %u týden" msgstr[1] "Přeskočeny %u týdny" msgstr[2] "Přeskočeno %u týdnů" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Přeskočen %u měsíc" msgstr[1] "Přeskočeny %u měsíce" msgstr[2] "Přeskočeno %u měsíců" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Dnes" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Včera" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Tento týden" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Týden %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Týden %u z %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Krátká přestávka" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Dlouhá přestávka" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Přestávka" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Otevřít překrytí obrazovky" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Relace vypršela" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Dlouhá přestávka za %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Přetočit o jednu minutu" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Kompaktní zobrazení" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Předvolby" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_O aplikaci" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Ukončit" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Hlavní nabídka" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Nechat časovač běžet?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Můžete jej nechat běžet na pozadí — oznámení a klávesové zkratky budou stále " "fungovat." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Spustit na pozadí" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Je čas na přestávku" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Hlavní okno" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Preferovat kompaktní zobrazení" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Spuštěno" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "Pozastaveno" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Upravit vlastní akci" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Zrušit" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Uložit" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Název" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Spouštěč" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Událost" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Spustit příkaz po události." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Podmínka" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Zajistit spuštění druhého příkazu, jakmile přestane být podmínka splněna." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Události" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Přidat _událost" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtr" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filtr" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Příkaz shellu" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Příkazy" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Příkaz při splnění podmínky" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Příkaz při nesplnění podmínky" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Pracovní adresář" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Použít subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Spustit program ze subshellu, např. sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Čekat na dokončení" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Blokovat spouštění ostatních příkazů, dokud tento příkaz neskončí." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Smazat akci" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Zatím nejsou určeny žádné události." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Přidat vlastní akci" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Přidat" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Vyberte pracovní adresář" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Vybrat" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Nepojmenovaná akce" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Přidat podmínku" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Přidat skupinu" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 #, fuzzy msgid "AND" msgstr "A" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 #, fuzzy msgid "OR" msgstr "NEBO" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Je" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Není" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Rovná se" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Větší než" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Menší než" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Ano" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Ne" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuty" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Sekundy" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Hodiny" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Vyberte pole…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Stav" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Běží" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Trvání" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Vložit proměnnou" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Formát" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Záznam" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Zobrazit záznam provádění" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Automaticky spouštět příkazy shellu při událostech nebo podmínkách časovače. " "Zjistit více." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Nastavit zkratku" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Nastavit" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Vypnuto" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Stiskněte Esc pro zrušení nebo Backspace pro vypnutí klávesové " "zkratky" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globální zkratky umožňují ovládat aplikaci, i když není na obrazovce. " "Fungují, dokud aplikace běží na pozadí." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Otevřít nastavení aplikace pro úpravu globálních zkratek" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Upravit" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Zadejte novou zkratku pro spuštění nebo zastavení časovače" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Zadejte novou zkratku pro spuštění/pozastavení/pokračování časovače" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Zadejte novou zkratku pro spuštění časovače" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Zadejte novou zkratku pro zastavení časovače" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Zadejte novou zkratku pro pozastavení časovače" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Zadejte novou zkratku pro pokračování v časovači" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Zadejte novou zkratku pro přeskočení" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Přetočit o minutu" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Zadejte novou zkratku pro přetáčení" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Zadejte novou zkratku pro přenesení okna do popředí" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Oznámení" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Čas vypršel" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Upozornit, když Pomodoro nebo přestávka brzy skončí." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Celoobrazovkové upozornění vynucující si přestávku." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Prodleva uzamčení" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Doba nečinnosti pro uzamčení obrazovky." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Prodleva znovuotevření" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Doba nečinnosti před znovuotevřením překrytí po jeho zavření." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Nikdy" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Upozornění" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Zvuky" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Vzhled" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Klávesové zkratky" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatizace" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Zvuky jsou vypnuty" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Zvuky upozornění" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Zvuk konce pomodora" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Zvuk konce přestávky" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Zvuk na pozadí" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Zvonek" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Hlasitý zvonek" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tikání hodin" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Žádný" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Hlasitost:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Vyberte vlastní zvuk" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Délka pomodora" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Délka krátké přestávky" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Délka dlouhé přestávky" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Počet cyklů" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Chování" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pozastavit při uzamčení obrazovky" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Potvrdit spuštění přestávky" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Potvrdit spuštění pomodora" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Jedna relace bude trvat %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% času bude vyhrazeno pro přestávky." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Použít změny na probíhající Pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Použít změny na probíhající přestávku?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Použít" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Boční panel" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Nástroj pro správu času" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Udržte soustředění díky pravidelným přestávkám" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Vizuální a zvuková upozornění" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Sledování času a statistiky" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integrace s desktopem GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Spouštění vlastních příkazů po skončení práce nebo přestávky" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Kompaktní časovač" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Přehled změn v gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Přidán tamilský překlad (díky @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Přidán hebrejský překlad (díky @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Přehled změn v gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Podpora pro GNOME Shell 49 (díky @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Aktualizován německý překlad (díky @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Přehled změn v gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Podpora pro GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Rozdělení času stráveného přes půlnoc" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Přidán telugský překlad (díky @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Přehled změn v gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Podpora pro GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Povolit skrytí překrytí obrazovky gestem během přehrávání videa" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Přidán gruzínský překlad (díky @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Upraveny překlady v appdata (díky @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Přehled změn v gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Oprava zachování oznámení po prodloužení Pomodora" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Přehled změn v gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Opravy pro GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Ukončena podpora pro GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Přehled změn v gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Podpora pro GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Upraven skript sestavení pro meson 0.59.0 (díky @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Nechte Pomodoro spravovat systémová oznámení během běhu časovače" #~ msgid "15 seconds" #~ msgstr "15 sekund" #~ msgid "30 seconds" #~ msgstr "30 sekund" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuta" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minuty" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minuty" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minut" #~ msgid "Timer Ticking" #~ msgstr "Tikání časovače" #, fuzzy #~ msgid "Birds" #~ msgstr "Ptáci" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "časovač;timer;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Start/Stop" #~ msgid "Pause/Resume" #~ msgstr "Pozastavit/Pokračovat" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Přeskočit na pomodoro nebo na přestávku" #~ msgid "Reset current session" #~ msgstr "Resetovat aktuální relaci" #, fuzzy #~ msgid "High Contrast Theme" #~ msgstr "Použít tmavý motiv" focustimerhq-FocusTimer-8581be2/po/de.po000066400000000000000000001721451520625676500202370ustar00rootroot00000000000000# German translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Johannes Hermann , 2013. # Tim Sabsch , 2018. # Philipp Kiemle , 2025. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2025-04-16 13:24+0200\n" "Last-Translator: Philipp Kiemle \n" "Language-Team: German\n" "Language: de_DE\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "X-Generator: Poedit 3.4.2\n" "X-Poedit-SourceCharset: UTF-8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Ein Produktivitäts-Timer, der Ihnen hilft, effektiver zu arbeiten, indem er " "Ihre Zeit in konzentrierte Arbeitssitzungen und kurze Pausen unterteilt. " "Arbeiten Sie 25 Minuten lang und machen Sie dann 5 Minuten Pause, um die " "Konzentration aufrechtzuerhalten und Burnout vorzubeugen." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Hauptmerkmale:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Anpassbare Längen für Arbeitssitzungen und Pausen" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Bildschirmüberlagerung während der Pausen" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Timer" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Tägliche Statistiken" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Monatliche Statistiken" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Einstellungen" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Bildschirmüberlagerung" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Starten oder Stoppen" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Starten, Pausieren oder Fortsetzen" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Pomodoro starten" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Start" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 #, fuzzy msgid "Stop" msgstr "Stopp" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pause" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 #, fuzzy msgid "Resume" msgstr "Fortsetzen" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Überspringen" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Zurückspulen" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Aktuelle(s) Pomodoro oder Pause verlängern" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Zurücksetzen" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Einstellungen anzeigen" #: src/application.vala:203 msgid "Quit application" msgstr "Anwendung beenden" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Versionsinformationen anzeigen und beenden" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "In den Fokus rücken" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s verbleibend" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Eigene Aktion \"%s\" ist fehlgeschlagen" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Zeitüberschreitung erreicht" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Befehl konnte nicht ausgeführt werden" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Befehl ist leer" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Nicht geschlossenes Anführungszeichen" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Ungültiger Befehl" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Unbekannte Variable \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Unbekanntes Format \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programm \"%s\" nicht gefunden" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Aktionen" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Countdown" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sitzung" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Andere" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Timer gestartet." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Timer manuell gestoppt." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Der Countdown wurde manuell pausiert. Wird nicht ausgelöst, wenn der " "Bildschirm gesperrt oder das System in den Standby versetzt wird." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Der Countdown wurde manuell fortgesetzt." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Zum nächsten Zeitblock gesprungen, bevor der Countdown beendet war." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Zurückspulen wurde verwendet. Es fügt eine Pause in der Vergangenheit hinzu." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Sitzung manuell geleert." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Beendet" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Der Countdown ist beendet. Falls auf eine Bestätigung gewartet wird, kann " "die Dauer des Zeitblocks noch geändert werden." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Geändert" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Wird bei jeder Änderung am Countdown ausgelöst." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Fortschritt bestätigen" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "" "Eine manuelle Bestätigung ist nötig, um den nächsten Zeitblock zu starten." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Fortgeschritten" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Zum nächsten Zeitblock gewechselt oder gesprungen." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Status geändert" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Zum nächsten Zeitblock gewechselt oder eine Pause wurde neu benannt." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Neu geplant" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Wird ausgelöst, wenn geplante Zeitblöcke geändert wurden." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Abgelaufen" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Wird ausgelöst, wenn die Sitzung wegen Inaktivität zurückgesetzt wird." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Machen Sie eine Pause" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Machen Sie eine kurze Pause" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Machen Sie eine lange Pause" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro endet bald" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Pause machen" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Die Pause endet bald" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 Minute" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Machen Sie sich bereit …" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Pomodoro ist vorbei!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Pause ist vorbei!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Start eines Pomodoro bestätigen …" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Start einer Pause bestätigen …" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Start einer kurzen Pause bestätigen …" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Start einer langen Pause bestätigen …" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Pause überspringen" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Wiedergabe konnte nicht initialisiert werden" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Datei nicht gefunden" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Dateityp nicht unterstützt" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Gestoppt" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pause" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Kurze Pause" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Lange Pause" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u Stunde" msgstr[1] "%u Stunden" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u Minute" msgstr[1] "%u Minuten" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u Sekunde" msgstr[1] "%u Sekunden" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Der genaue Zeitpunkt des aktuellen Ereignisses." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Die aktuelle Phase des Pomodoro-Zyklus. Mögliche Werte: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status des aktuellen Zeitblocks. Mögliche Werte: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Gibt an, ob der Countdown begonnen hat." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Gibt an, ob der Countdown pausiert ist." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Gibt an, ob der Countdown beendet ist." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Gibt an, ob der Timer aktiv herunterzählt." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Dauer des aktuellen Countdowns." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "" "Abweichung zwischen verstrichener Zeit und tatsächlich vergangener Zeit." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Die im Countdown verbrachte Zeit." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Die verbleibende Zeit bis zum Ende des Countdowns." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Zeitpunkt, an dem der Countdown gestartet wurde." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell Erweiterung" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Holen Sie das Beste heraus!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Aktivieren Sie die GNOME Shell Erweiterung für nahtlose Desktop-" "Integration" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Immer in Reichweite" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "" "Steuern Sie den Timer direkt aus der oberen Leiste, ohne die App zu öffnen" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Weniger Ablenkungen" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Verbesserte Pausenerinnerungen" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegante Vollbild-Überlagerung, die Pausen zu einer angenehmeren Erfahrung " "macht" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Bereit, es zu versuchen?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "Erweiterung _installieren" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "Jetzt _nicht" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Etwas ist schiefgelaufen" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "In Zwischenablage kopieren" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "Erneut _versuchen" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Abbrechen" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Zeitüberschreitung erreicht" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Installation von Erweiterungen ist nicht erlaubt" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Herunterladen der Erweiterung fehlgeschlagen" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Bildschirmüberlagerung" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Arbeitsumgebung" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell Erweiterung verfügbar" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Mehr erfahren" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Unbenutzt" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Fertig!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistiken" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Beenden" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Protokoll" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Leeres Protokoll" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Einträge erscheinen hier, sobald Sie den Timer starten." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Kontext" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Befehl" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Ausgabe" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Fehler" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Beendigungscode:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Ausführungszeit:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "Jonatan Zeidler \n" "Johannes Hermann \n" "Tim Sabsch \n" "Philipp Kiemle " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Spenden" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pausen" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Unterbrechungen" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Pausenverhältnis" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Tag" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Woche" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Monat" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Noch nichts zu sehen" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Beenden Sie ein paar Pomodoros, um dies zu füllen!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u Tag übersprungen" msgstr[1] "%u Tage übersprungen" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u Woche übersprungen" msgstr[1] "%u Wochen übersprungen" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u Monat übersprungen" msgstr[1] "%u Monate übersprungen" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Heute" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Gestern" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Diese Woche" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Woche %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Woche %u von %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Kurze Pause" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Lange Pause" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pause" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Bildschirmüberlagerung öffnen" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Sitzung ist abgelaufen" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Lange Pause fällig in %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Eine Minute zurückspulen" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Kompakte Ansicht" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Einstellungen" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Info" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Beenden" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Hauptmenü" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Timer weiterlaufen lassen?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Sie können ihn im Hintergrund weiterlaufen lassen — Benachrichtigungen und " "Tastenkürzel funktionieren weiterhin." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Im Hintergrund ausführen" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Es ist Zeit, eine Pause einzulegen" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Hauptfenster" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Dunkles Thema bevorzugen" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Kompakte Ansicht bevorzugen" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Gestartet" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Angehalten" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Eigene Aktion bearbeiten" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Abbrechen" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Speichern" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Name" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Auslöser" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Ereignis" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Befehl nach einem Ereignis ausführen." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Bedingung" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Ausführung eines zweiten Befehls sicherstellen, sobald die Bedingung nicht " "mehr erfüllt ist." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Ereignisse" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "_Ereignis hinzufügen" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtern" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell-Befehl" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Befehle" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Befehl bei erfüllter Bedingung" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Befehl bei nicht erfüllter Bedingung" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Arbeitsverzeichnis" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Subshell verwenden" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Programm aus einer Subshell wie sh -c '' ausführen" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Eingabedaten übergeben" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Statt Variablen zu übergeben, können Sie ein JSON-Objekt verarbeiten." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Auf Beendigung warten" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "" "Ausführung anderer Befehle blockieren, bis der Befehl abgeschlossen ist." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "Aktion _löschen" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Noch keine Ereignisse angegeben." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Eigene Aktion hinzufügen" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Hinzufügen" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Arbeitsverzeichnis wählen" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "Au_swählen" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Unbenannte Aktion" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Bedingung hinzufügen" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Gruppe hinzufügen" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 #, fuzzy msgid "AND" msgstr "UND" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 #, fuzzy msgid "OR" msgstr "ODER" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Ist" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Ist nicht" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Gleicht" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Größer als" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Kleiner als" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Ja" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Nein" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuten" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Sekunden" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Stunden" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Feld wählen …" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Status" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Läuft" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Dauer" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Variable einfügen" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Protokoll" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Ausführungsprotokoll anzeigen" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Shell-Befehle automatisch bei Timer-Ereignissen oder Bedingungen ausführen. " "Mehr erfahren." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Tastenkürzel festlegen" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Festlegen" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Deaktiviert" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Drücken Sie Esc zum Abbrechen oder Rücktaste, um das " "Tastenkürzel zu deaktivieren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globale Tastenkürzel lassen Sie die App steuern, auch wenn sie nicht auf dem " "Bildschirm ist. Sie funktionieren, solange die App im Hintergrund läuft." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "App-Einstellungen öffnen, um globale Tastenkürzel zu bearbeiten" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Bearbeiten" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Neues Tastenkürzel zum Starten oder Stoppen des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Neues Tastenkürzel zum Starten/Pausieren/Fortsetzen des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Neues Tastenkürzel zum Starten des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Neues Tastenkürzel zum Stoppen des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Neues Tastenkürzel zum Pausieren des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Neues Tastenkürzel zum Fortsetzen des Timers eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Neues Tastenkürzel zum Überspringen eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Eine Minute zurückspulen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Neues Tastenkürzel zum Zurückspulen eingeben" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Neues Tastenkürzel eingeben, um das Fenster in den Fokus zu rücken" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Ankündigungen" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Zeit läuft ab" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Benachrichtigen, wenn Pomodoro oder Pause bald enden." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "" "Eine Vollbild-Benachrichtigung, um das Einlegen einer Pause zu erzwingen." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Sperrverzögerung" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Zeitraum der Inaktivität, nach dem der Bildschirm gesperrt wird." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Wiederöffnungsverzögerung" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Zeitraum der Inaktivität, nach dem die Überlagerung nach dem Schließen " "wieder geöffnet wird." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Nie" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Benachrichtigungen" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Töne" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Erscheinungsbild" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Tastenkürzel" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatisierung" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Töne sind deaktiviert" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Alarmtöne" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Ton bei beendetem Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Ton bei beendeter Pause" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Hintergrundton" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Glocke" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Laute Glocke" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Uhrticken" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Keiner" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Lautstärke:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Benutzerdefinierten Klang wählen" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Pomodoro-Dauer" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Kurze Pausendauer" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Lange Pausendauer" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Anzahl der Zyklen" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Verhalten" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pausieren beim Sperren des Bildschirms" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Start einer Pause bestätigen" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Start eines Pomodoro bestätigen" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Eine einzelne Sitzung dauert %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% der Zeit wird für Pausen aufgewendet." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Änderungen auf laufendes Pomodoro anwenden?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Änderungen auf laufende Pause anwenden?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Anwenden" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Seitenleiste" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Werkzeug für das Zeitmanagement" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Konzentriert bleiben durch regelmäßige Pausen" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Visuelle und akustische Benachrichtigungen" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Zeiterfassung und Statistiken" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integration in den GNOME-Desktop" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Eigene Befehle nach einem Pomodoro oder einer Pause ausführen" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Kompakter Timer" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Tamilische Übersetzung hinzugefügt (Danke an @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Hebräische Übersetzung hinzugefügt (Danke an @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Unterstützung für GNOME Shell 49 (Danke an @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Deutsche Übersetzung aktualisiert (Danke an @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Unterstützung für GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Zeitaufwand über Mitternacht hinweg aufteilen" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Telugu-Übersetzung hinzugefügt (Danke an @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Unterstützung für GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Erlaube das Schließen der Überlagerung per Geste während ein Video läuft" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Georgische Übersetzung hinzugefügt (Danke an @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Übersetzungen in den App-Metadaten angepasst (Danke an @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Fehler behoben: Benachrichtigung blieb nach Verlängerung bestehen" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Fehlerbehebungen für GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Unterstützung für GNOME Shell 45 eingestellt" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Übersicht der Änderungen in gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Unterstützung für GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Build-Skript auf Meson 0.59.0 angepasst (Danke an @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Lassen Sie Pomodoro die Systembenachrichtigungen verwalten, " #~ "während der Timer läuft" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 Sekunden" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 Sekunden" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 Minute" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 Minuten" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 Minuten" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 Minuten" #~ msgid "Timer Ticking" #~ msgstr "Stoppuhrticken" #, fuzzy #~ msgid "Birds" #~ msgstr "Vögel" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "Timer;Eieruhr;Stoppuhr;" #~ msgid "Start/Stop" #~ msgstr "Start/Stopp" #, fuzzy #~ msgid "Pause/Resume" #~ msgstr "Pause/Fortsetzen" #, fuzzy #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Zu einem Pomodoro oder zu einer Pause springen" #~ msgid "Reset current session" #~ msgstr "Aktuelle Sitzung zurücksetzen" #~ msgid "Run as background service" #~ msgstr "Als Hintergrunddienst ausführen" #~ msgid "About Pomodoro" #~ msgstr "Info zu Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Ein einfaches Werkzeug für Zeitmanagement" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Stop" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indikator in der GNOME-Shell" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Konnte die Erweiterung nicht aktivieren" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Dauer einer langen Pause" #~ msgid "A time management utility for GNOME" #~ msgstr "Ein einfaches Werkzeug für Zeitmanagement unter GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Eine GNOME-Erweiterung, die Zeitmanagement nach der Pomodoro-Technik " #~ "unterstützt. Mit fokussierten Arbeitseinheiten und kleinen Pausen nach 25 " #~ "Minuten soll die Produktivität erhöht werden." #~ msgid "Timer window" #~ msgstr "Timer-Fenster" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indikator in der GNOME-Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indikator in der GNOME-Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indikator in der GNOME-Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indikator in der GNOME-Shell" #~ msgid "_Timer" #~ msgstr "_Timer" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Tastenkombination, um den Timer ein-/auszuschalten. Zum Ändern neu " #~ "eingeben." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoros bis zu einer langen Pause" #~ msgid "Keyboard shortcut" #~ msgstr "Tastenkürzel" #~ msgid "Screen notifications" #~ msgstr "Bildschirmbenachrichtigungen" #~ msgid "Wait for activity after a break" #~ msgstr "Nach einer Pause auf Aktivität warten" #~ msgid "Plugins…" #~ msgstr "Erweiterungen …" #~ msgid "Plugins" #~ msgstr "Erweiterungen" #~ msgid "Back" #~ msgstr "Zurück" #~ msgid "Complete a few sessions" #~ msgstr "Absolvieren Sie ein paar Sitzungen" #~ msgid "Previous (Alt+Left)" #~ msgstr "Zurück (Alt+Links)" #~ msgid "Next (Alt+Right)" #~ msgstr "Vor (Alt+Rechts)" #~ msgid "Complete" #~ msgstr "Vollständig" #~ msgid "Enable" #~ msgstr "Aktivieren" #~ msgid "Add" #~ msgstr "Hinzufügen" #~ msgid "Remove" #~ msgstr "Entfernen" #~ msgid "Elapsed Time" #~ msgstr "Vergangene Zeit" #~ msgid "Pause Timer" #~ msgstr "Timer unterbrechen" #~ msgid "Pause break" #~ msgstr "Pause unterbrechen" #~ msgid "Pause Pomodoro" #~ msgstr "Pomodoro unterbrechen" #~ msgid "Resume break" #~ msgstr "Pause fortsetzen" #~ msgid "Resume Pomodoro" #~ msgstr "Pomodoro fortsetzen" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d Minute verbleibend" #~ msgstr[1] "%d Minuten verbleibend" #~ msgid "Report issue" #~ msgstr "Problem melden" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Service %s konnte nicht ausgeführt werden" #~ msgid "Woodland Birds" #~ msgstr "Vögel" #~ msgid "End of Break Sound" #~ msgstr "Klang für Pausenende" #~ msgid "Start of Break Sound" #~ msgstr "Klang für Pausenanfang" #~ msgid "Off" #~ msgstr "Aus" #~ msgid "Ticking sound" #~ msgstr "Ticken" #~ msgid "Start of break sound" #~ msgstr "Klang für Pausenanfang" #~ msgid "End of break sound" #~ msgstr "Klang für Pausenende" #~ msgid "Focus on your task." #~ msgstr "Konzentrieren Sie sich auf Ihre Arbeit." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Sie haben %d Minute" #~ msgstr[1] "Sie haben %d Minuten" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Sie haben %d Sekunde" #~ msgstr[1] "Sie haben %d Sekunden" #~ msgid "Take a longer break" #~ msgstr "Machen Sie eine längere Pause" #~ msgid "Lengthen it" #~ msgstr "Verlängern" #~ msgid "Shorten it" #~ msgstr "Verkürzen" #~ msgid "Start pomodoro" #~ msgstr "Pomodoro beginnen" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Das Tastenkürzel \"%s\" wird beim Tippen stören. Versuchen Sie, eine " #~ "andere Taste hinzuzufügen, z. B. Strg, Alt oder Umschalt." #~ msgid "Available" #~ msgstr "Verfügbar" #~ msgid "Busy" #~ msgstr "Beschäftigt" #~ msgid "Idle" #~ msgstr "Inaktiv" #~ msgid "Invisible" #~ msgstr "Unsichtbar" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Statistiken" #~ msgid "It seems to be uninstalled" #~ msgstr "Die Erweiterung scheint deinstalliert zu sein" #~ msgid "Extension is out of date" #~ msgstr "Erweiterung ist nicht aktuell" #~ msgid "Upgrade" #~ msgstr "Aktualisierung" #~ msgid "Remind to take a break" #~ msgstr "An Pausen erinnern" #~ msgid "Extras" #~ msgstr "Extras" #~ msgid "Remove Sound" #~ msgstr "Keine Klänge" #~ msgid "00" #~ msgstr "00" #~ msgid ":" #~ msgstr ":" #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d neue Nachricht" #~ msgstr[1] "%d neue Nachrichten" #~ msgid "Take a break!" #~ msgstr "Mache eine Pause!" #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Du hast noch %d Minute bis zur nächsten Pomodoro" #~ msgstr[1] "Du hast noch %d Minuten bis zur nächsten Pomodoro" #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Du hast noch %d Sekunde bis zur nächsten Pomodoro" #~ msgstr[1] "Du hast noch %d Sekunden bis zur nächsten Pomodoro" #~ msgid "Hey!" #~ msgstr "Hallo!" #~ msgid "You're missing out on a break" #~ msgstr "Du hast die Pause verpasst" focustimerhq-FocusTimer-8581be2/po/el.po000066400000000000000000002074131520625676500202440ustar00rootroot00000000000000# Greek translation for focus-timer # Copyright (c) 2016 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Giannis Katsanos , 2016. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 15:40+0100\n" "Last-Translator: Giannis Katsanos \n" "Language-Team: Greek\n" "Language: el\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Ένα χρονόμετρο παραγωγικότητας που σας βοηθά να εργάζεστε πιο αποτελεσματικά " "χωρίζοντας τον χρόνο σας σε περιόδους συγκεντρωμένης εργασίας που " "ακολουθούνται από σύντομα διαλείμματα. Εργαστείτε για 25 λεπτά και μετά " "κάντε ένα διάλειμμα 5 λεπτών για να διατηρήσετε τη συγκέντρωσή σας." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Κύρια χαρακτηριστικά:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Προσαρμόσιμη διάρκεια εργασίας και διαλειμμάτων" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Επικάλυψη οθόνης κατά τη διάρκεια των διαλειμμάτων" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Χρονόμετρο" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Ημερήσια στατιστικά" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Μηνιαία στατιστικά" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Προτιμήσεις" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Επικάλυψη οθόνης" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Έναρξη ή Διακοπή" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Έναρξη, Παύση ή Συνέχεια" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Έναρξη Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Έναρξη" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Διακοπή" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Παύση" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Συνέχεια" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 #, fuzzy msgid "Skip" msgstr "Παράλειψη" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Επανατύλιξη" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 #, fuzzy msgid "Extend current pomodoro or break" msgstr "Επέκταση τρέχοντος Pomodoro ή διαλείμματος" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Επαναφορά" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Εμφάνιση προτιμήσεων" #: src/application.vala:203 msgid "Quit application" msgstr "Κλείσιμο εφαρμογής" #: src/application.vala:206 #, fuzzy msgid "Print version information and exit" msgstr "Εμφάνιση έκδοσης και έξοδος" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Εστίαση παραθύρου" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "απομένουν %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Η προσαρμοσμένη ενέργεια \"%s\" απέτυχε" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Έληξε το χρονικό όριο" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Αποτυχία εκτέλεσης εντολής" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Η εντολή είναι κενή" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Μη κλεισμένα εισαγωγικά" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Μη έγκυρη εντολή" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Άγνωστη μεταβλητή \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Άγνωστη μορφή \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Το πρόγραμμα \"%s\" δεν βρέθηκε" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Ενέργειες" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Αντίστροφη μέτρηση" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Συνεδρία" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Άλλο" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Το χρονόμετρο ξεκίνησε." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Το χρονόμετρο διακόπηκε χειροκίνητα." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Η αντίστροφη μέτρηση παύθηκε χειροκίνητα. Δεν ενεργοποιείται κατά το " "κλείδωμα της οθόνης ή την αναστολή του συστήματος." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Η αντίστροφη μέτρηση συνεχίστηκε χειροκίνητα." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "" "Μετάβαση στο επόμενο χρονικό μπλοκ πριν τελειώσει η αντίστροφη μέτρηση." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Χρησιμοποιήθηκε η ενέργεια επανατύλιξης. Προσθέτει μια παύση στο παρελθόν." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Η συνεδρία καθαρίστηκε χειροκίνητα." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Ολοκληρώθηκε" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Η αντίστροφη μέτρηση τελείωσε. Εάν αναμένεται επιβεβαίωση, η διάρκεια μπορεί " "ακόμη να τροποποιηθεί." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Άλλαξε" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "" "Ενεργοποιείται σε οποιαδήποτε αλλαγή σχετική με την αντίστροφη μέτρηση." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Επιβεβαίωση μετάβασης" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Απαιτείται χειροκίνητη επιβεβαίωση για την έναρξη του επόμενου μπλοκ." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Προχώρησε" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Μετάβαση ή παράλειψη στο επόμενο χρονικό μπλοκ." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Η κατάσταση άλλαξε" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Μετάβαση σε επόμενο μπλοκ ή αλλαγή ετικέτας διαλείμματος." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Επαναπρογραμματίστηκε" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Ενεργοποιείται όταν τα προγραμματισμένα μπλοκ αλλάζουν." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Έληξε" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Ενεργοποιείται όταν η συνεδρία πρόκειται να μηδενιστεί λόγω αδράνειας." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 #, fuzzy msgid "Take a break" msgstr "Κάντε ένα διάλειμμα" #: src/core/notification-manager.vala:355 #, fuzzy msgid "Take a short break" msgstr "Κάντε ένα σύντομο διάλειμμα" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Κάντε ένα μεγάλο διάλειμμα" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Το Pomodoro τελειώνει" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Κάντε ένα Διάλειμμα" #: src/core/notification-manager.vala:425 #, fuzzy msgid "Break is about to end" msgstr "Το διάλειμμα τελειώνει" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 λεπτό" #: src/core/notification-manager.vala:458 #, fuzzy msgid "Get ready…" msgstr "Ετοιμαστείτε…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Το Pomodoro τελείωσε!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Το διάλειμμα τελείωσε!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Επιβεβαιώστε την έναρξη του Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Επιβεβαιώστε την έναρξη του διαλείμματος…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Επιβεβαιώστε την έναρξη σύντομου διαλείμματος…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Επιβεβαιώστε την έναρξη μεγάλου διαλείμματος…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Παράλειψη διαλείμματος" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Αποτυχία προετοιμασίας αναπαραγωγής" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Το αρχείο δεν βρέθηκε" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Ο τύπος αρχείου δεν υποστηρίζεται" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Διακόπηκε" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Διάλειμμα" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Σύντομο Διάλειμμα" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Μεγάλο Διάλειμμα" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uώ" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%uλ" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u ώρα" msgstr[1] "%u ώρες" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u λεπτό" msgstr[1] "%u λεπτά" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u δευτερόλεπτο" msgstr[1] "%u δευτερόλεπτα" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Η ακριβής ώρα του τρέχοντος γεγονότος." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Η τρέχουσα φάση του κύκλου. Τιμές: stopped, pomodoro, " "break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Κατάσταση του χρονικού μπλοκ. Τιμές: scheduled, in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Δείκτης εάν έχει ξεκινήσει η αντίστροφη μέτρηση." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Δείκτης εάν η αντίστροφη μέτρηση είναι σε παύση." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Δείκτης εάν η αντίστροφη μέτρηση έχει τελειώσει." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Δείκτης εάν το χρονόμετρο μετράει ενεργά." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Διάρκεια της τρέχουσας αντίστροφης μέτρησης." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Απόκλιση μεταξύ του χρόνου που πέρασε και του πραγματικού χρόνου." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Ο χρόνος που δαπανήθηκε στην αντίστροφη μέτρηση." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Ο χρόνος που απομένει μέχρι το τέλος." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Η ώρα που ξεκίνησε η αντίστροφη μέτρηση." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Επέκταση GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Αποκτήστε την καλύτερη εμπειρία!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Ενεργοποιήστε την επέκταση GNOME Shell για πλήρη ενσωμάτωση" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Πάντα άμεσα προσβάσιμο" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Ελέγξτε το χρονόμετρο από την πάνω μπάρα χωρίς άνοιγμα της εφαρμογής" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Λιγότεροι περισπασμοί" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Βελτιωμένες υπενθυμίσεις διαλείμματος" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Κομψή επικάλυψη οθόνης για μια πιο ευχάριστη εμπειρία διαλείμματος" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Έτοιμοι να το δοκιμάσετε;" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Εγκατάσταση επέκτασης" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Όχι τώρα" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Κάτι πήγε στραβά" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Αντιγραφή στο πρόχειρο" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Δοκιμάστε ξανά" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Ματαίωση" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Έληξε το χρονικό όριο" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Η εγκατάσταση επεκτάσεων δεν επιτρέπεται" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Αποτυχία λήψης της επέκτασης" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Επικάλυψη οθόνης" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Επιφάνεια εργασίας" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Διαθέσιμη επέκταση GNOME Shell" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Μάθετε περισσότερα" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Μη χρησιμοποιούμενο" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Ολοκληρώθηκε!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Στατιστικά" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Κλείσιμο" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Καταγραφή" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Κενή καταγραφή" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Οι καταχωρήσεις θα εμφανιστούν εδώ όταν ξεκινήσετε το χρονόμετρο." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Πλαίσιο" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Εντολή" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Έξοδος" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Σφάλμα" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Κωδικός εξόδου:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Χρόνος εκτέλεσης:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Γιάννης Κατσάνος" #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Δωρεά" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Διαλείμματα" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Διακοπές" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Αναλογία διαλείμματος" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Ημέρα" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Εβδομάδα" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Μήνας" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Δεν υπάρχει κάτι εδώ ακόμα" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Ολοκληρώστε μερικά Pomodoro για να γεμίσει αυτό!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Παραλείφθηκε %u ημέρα" msgstr[1] "Παραλείφθηκαν %u ημέρες" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Παραλείφθηκε %u εβδομάδα" msgstr[1] "Παραλείφθηκαν %u εβδομάδες" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Παραλείφθηκε %u μήνας" msgstr[1] "Παραλείφθηκαν %u μήνες" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Σήμερα" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Εχθές" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Αυτή την εβδομάδα" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Εβδομάδα %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Εβδομάδα %u από %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Σύντομο Διάλειμμα" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Μεγάλο Διάλειμμα" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Διάλειμμα" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Άνοιγμα επικάλυψης οθόνης" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Η συνεδρία έληξε" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Μεγάλο διάλειμμα σε %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Επανατύλιξη ένα λεπτό" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Συμπαγής προβολή" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Προτιμήσεις" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Σχετικά" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Κλείσιμο" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Κύριο μενού" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Να συνεχίσει το χρονόμετρο;" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Μπορείτε να το αφήσετε να τρέχει στο παρασκήνιο — οι ειδοποιήσεις και οι " "συντομεύσεις θα συνεχίσουν να λειτουργούν." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Εκτέλεση στο παρασκήνιο" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Ώρα για διάλειμμα" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Κύριο παράθυρο" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Προτίμηση σκούρου θέματος" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Προτίμηση συμπαγούς προβολής" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Ξεκίνησε" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "Σε παύση" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Επεξεργασία προσαρμοσμένης ενέργειας" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Ματαίωση" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Αποθήκευση" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Όνομα" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Έναυσμα" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Συμβάν" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Εκτέλεση εντολής μετά από ένα συμβάν." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Συνθήκη" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "Εκτέλεση δεύτερης εντολής όταν η συνθήκη δεν πληρούται πλέον." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Συμβάντα" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Προσθήκη _Συμβάντος" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Φιλτράρισμα" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Φίλτρο" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Εντολή Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Εντολές" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Εντολή πληρούμενης συνθήκης" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Εντολή μη πληρούμενης συνθήκης" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Φάκελος εργασίας" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Χρήση Subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Εκτέλεση του προγράμματος μέσω subshell (π.χ. sh -c)" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Μεταβίβαση δεδομένων εισόδου" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Αντί για μεταβλητές, μπορείτε να επεξεργαστείτε ένα αντικείμενο JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Αναμονή για ολοκλήρωση" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Φραγή εκτέλεσης άλλων εντολών μέχρι την ολοκλήρωση." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Διαγραφή ενέργειας" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Δεν έχουν οριστεί συμβάντα ακόμα." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Προσθήκη προσαρμοσμένης ενέργειας" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Προσθήκη" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Επιλογή φακέλου εργασίας" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Επιλογή" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Ενέργεια χωρίς τίτλο" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Προσθήκη συνθήκης" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Προσθήκη ομάδας" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "ΚΑΙ" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "Ή" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Είναι" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Δεν είναι" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Ισούται με" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Μεγαλύτερο από" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Μικρότερο από" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Ναι" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Όχι" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Λεπτά" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Δευτερόλεπτα" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Ώρες" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Επιλογή πεδίου…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Κατάσταση" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Σε λειτουργία" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Διάρκεια" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Εισαγωγή μεταβλητής" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Μορφή" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Καταγραφή" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Εμφάνιση καταγραφής εκτέλεσης" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Αυτόματη εκτέλεση εντολών shell σε συμβάντα ή συνθήκες. Μάθετε " "περισσότερα." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Ορισμός συντόμευσης" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Ορισμός" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Απενεργοποιημένο" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Πατήστε Esc για ακύρωση ή Backspace για απενεργοποίηση της " "συντόμευσης" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Οι παγκόσμιες συντομεύσεις επιτρέπουν τον έλεγχο της εφαρμογής ακόμα και στο " "παρασκήνιο." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Άνοιγμα ρυθμίσεων για επεξεργασία παγκόσμιων συντομεύσεων" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Επεξεργασία" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Εισαγωγή νέας συντόμευσης για έναρξη ή διακοπή" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Εισαγωγή νέας συντόμευσης για έναρξη/παύση/συνέχεια" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Εισαγωγή νέας συντόμευσης για έναρξη" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Εισαγωγή νέας συντόμευσης για διακοπή" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Εισαγωγή νέας συντόμευσης για παύση" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Εισαγωγή νέας συντόμευσης για συνέχεια" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Εισαγωγή νέας συντόμευσης για παράλειψη" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Επανατύλιξη ένα λεπτό" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Εισαγωγή νέας συντόμευσης για επανατύλιξη" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Εισαγωγή νέας συντόμευσης για εστίαση παραθύρου" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Ανακοινώσεις" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Ο χρόνος τελειώνει" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Ειδοποίηση όταν το Pomodoro ή το διάλειμμα τελειώνει." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Μια ειδοποίηση πλήρους οθόνης για την επιβολή διαλείμματος." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Καθυστέρηση κλειδώματος" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Περίοδος αδράνειας για το κλείδωμα της οθόνης." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Καθυστέρηση επανανοίγματος" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Περίοδος αδράνειας για την εκ νέου εμφάνιση της επικάλυψης." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Ποτέ" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Ειδοποιήσεις" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Ήχοι" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Εμφάνιση" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Συντομεύσεις πληκτρολογίου" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Αυτοματοποίηση" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Οι ήχοι είναι απενεργοποιημένοι" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Ήχοι ειδοποίησης" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Ήχος ολοκλήρωσης Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Ήχος ολοκλήρωσης διαλείμματος" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Ήχος παρασκηνίου" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Καμπάνα" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Δυνατή καμπάνα" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Χτύπος ρολογιού" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Κανένας" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Ένταση:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Επιλογή εξατομικευμένου ήχου" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Διάρκεια Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Διάρκεια σύντομου διαλείμματος" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Διάρκεια μεγάλου διαλείμματος" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Αριθμός κύκλων" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Συμπεριφορά" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Παύση κατά το κλείδωμα της οθόνης" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Επιβεβαίωση έναρξης διαλείμματος" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Επιβεβαίωση έναρξης Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Μια μεμονωμένη συνεδρία θα διαρκέσει %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "Το %u%% του χρόνου θα διατεθεί για διαλείμματα." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Εφαρμογή αλλαγών στο τρέχον Pomodoro;" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Εφαρμογή αλλαγών στο τρέχον διάλειμμα;" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Εφαρμογή" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Πλευρική στήλη" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Εργαλείο διαχείρισης χρόνου" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Διατηρήστε τη συγκέντρωσή σας κάνοντας συχνά διαλείμματα" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Οπτικές και ηχητικές ειδοποιήσεις" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Καταγραφή χρόνου και στατιστικά" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Ενσωμάτωση στο περιβάλλον GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Εκτέλεση προσαρμοσμένων εντολών μετά από Pomodoro ή διάλειμμα" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Συμπαγές χρονόμετρο" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Προσθήκη Ταμίλ μετάφρασης (ευχαριστούμε @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Προσθήκη Εβραϊκής μετάφρασης (ευχαριστούμε @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Υποστήριξη για το GNOME Shell 49 (ευχαριστούμε @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Ενημερωμένη Γερμανική μετάφραση (ευχαριστούμε @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Υποστήριξη για το GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Διαχωρισμός χρόνου που δαπανάται κατά τα μεσάνυχτα" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Προσθήκη Τελούγκου μετάφρασης (ευχαριστούμε @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Υποστήριξη για το GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Επιτρέπεται η παράλειψη της επικάλυψης με χειρονομία κατά την αναπαραγωγή " #~ "βίντεο" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Προσθήκη Γεωργιανής μετάφρασης (ευχαριστούμε @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Προσαρμογή μεταφράσεων στα appdata (ευχαριστούμε @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Διόρθωση παραμονής ειδοποίησης μετά την επέκταση του Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Διορθώσεις για το GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Διακοπή υποστήριξης για το GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Επισκόπηση αλλαγών στο gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Υποστήριξη για το GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Προσαρμογή σεναρίου δόμησης στο meson 0.59.0 (ευχαριστούμε @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Αφήστε το Pomodoro να διαχειρίζεται τις ειδοποιήσεις όσο τρέχει" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 δευτερόλεπτα" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 δευτερόλεπτα" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 λεπτό" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 λεπτά" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 λεπτά" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 λεπτά" #~ msgid "Timer Ticking" #~ msgstr "Χτύπος χρονομέτρου" #, fuzzy #~ msgid "Birds" #~ msgstr "Πουλιά" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "χρονόμετρο;timer;" #~ msgid "Start/Stop" #~ msgstr "Έναρξη/Διακοπή" #~ msgid "Pause/Resume" #~ msgstr "Παύση/Συνέχεια" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Μετάβαση σε ένα Pomodoro ή σε ένα διάλειμμα" #, fuzzy #~ msgid "Reset current session" #~ msgstr "Επαναφορά τρέχουσας συνεδρίας" #~ msgid "Run as background service" #~ msgstr "Εκτέλεση στο παρασκήνιο" #~ msgid "About Pomodoro" #~ msgstr "Σχετικά με το Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Μια απλή εφαρμογή διαχείρισης χρόνου" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Διακοπή" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Ένδειξη για το GNOME Shell" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Αποτυχία ενεργοποίησης πρόσθετου" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Διάρκεια μεγάλου διαλείμματος" #~ msgid "A time management utility for GNOME" #~ msgstr "Μια εφαρμογή διαχείρισης χρόνου για το GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Μια εφαρμογή του GNOME για διαχείριση χρόνου σύμφωνα με την τεχνική " #~ "Pomodoro. Ο σκοπός της είναι η βελτίωση της παραγωγικότητας και ενίσχυση " #~ "της συγκέντρωσης μέσω μικρών διαλειμμάτων μετά από κάθε 25 λεπτά δουλειάς." #~ msgid "Timer window" #~ msgstr "Παράθυρο χρονοδιακόπτη" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Ένδειξη για το GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Ένδειξη για το GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Ένδειξη για το GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Ένδειξη για το GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Χρονόμετρο" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Συντόμευση πληκτρολογίου για την έναρξη/παύση του χρονόμετρου. Εισάγετε " #~ "νέο συνδυασμό για αλλαγή." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoros πριν από ένα μεγάλο διάλειμμα" #~ msgid "Keyboard shortcut" #~ msgstr "Συντόμευση πληκτρολογίου" #~ msgid "Screen notifications" #~ msgstr "Ειδοποιήσεις οθόνης" #~ msgid "Wait for activity after a break" #~ msgstr "Ανίχνευση κίνησης μετά από διάλειμμα" #~ msgid "Plugins…" #~ msgstr "Πρόσθετα…" #~ msgid "Plugins" #~ msgstr "Πρόσθετα" #~ msgid "Back" #~ msgstr "Πίσω" #~ msgid "Complete a few sessions" #~ msgstr "Ολοκληρώστε μερικές συνεδρίες" #~ msgid "Previous (Alt+Left)" #~ msgstr "Προηγούμενο (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "Επόμενο (Alt+Right)" #~ msgid "Complete" #~ msgstr "Πλήρης" #~ msgid "Enable" #~ msgstr "Επιτρέπω" #~ msgid "Add" #~ msgstr "Προσθήκη" #~ msgid "Remove" #~ msgstr "Αφαιρώ" #~ msgid "Elapsed Time" #~ msgstr "Χρόνος που παρήλθε" #~ msgid "Pause Timer" #~ msgstr "Παύση χρονοδιακόπτη" #~ msgid "Pause break" #~ msgstr "Παύση" #~ msgid "Pause Pomodoro" #~ msgstr "Παύση" #~ msgid "Resume break" #~ msgstr "Χρονόμετρο συνέχισης" #~ msgid "Resume Pomodoro" #~ msgstr "Χρονόμετρο συνέχισης" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "Απομένει %d λεπτό" #~ msgstr[1] "Απομένουν %d λεπτά" #~ msgid "Report issue" #~ msgstr "Αναφορά προβλήματος" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Απέτυχε η εκτέλεση της υπηρεσίας %s" #~ msgid "Woodland Birds" #~ msgstr "Πουλιά" #~ msgid "End of Break Sound" #~ msgstr "Ήχος για το Τέλος Διαλείμματος" #~ msgid "Start of Break Sound" #~ msgstr "Ήχος για την Έναρξη Διαλείμματος" #~ msgid "Off" #~ msgstr "Απενεργοποίηση" #~ msgid "Ticking sound" #~ msgstr "Ήχος Ρολογιού" #~ msgid "Start of break sound" #~ msgstr "Ήχος για την έναρξη διαλείμματος" #~ msgid "End of break sound" #~ msgstr "Ήχος για το τέλος διαλείμματος" #~ msgid "Focus on your task." #~ msgstr "Συγκεντρώσου στη δουλειά σου." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Έχεις %d λεπτό" #~ msgstr[1] "Έχεις %d λεπτά" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Έχεις %d δευτερόλεπτο" #~ msgstr[1] "Έχεις %d δευτερόλεπτα" #~ msgid "Take a longer break" #~ msgstr "Κάνε ένα μεγάλο διάλειμμα" #~ msgid "Lengthen it" #~ msgstr "Επέκταση" #~ msgid "Shorten it" #~ msgstr "Σύμπτυξη" #~ msgid "Start pomodoro" #~ msgstr "Έναρξη pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Η χρήση του \"%s\" ως συντόμευση θα δημιουργήσει προβλήματα κατά την " #~ "πληκτρολόγηση. Δοκιμάστε κάποιο άλλο πλήκτρο όπως Control, Alt ή Shift." #~ msgid "Available" #~ msgstr "Διαθέσιμος" #~ msgid "Busy" #~ msgstr "Απασχολημένος" #~ msgid "Idle" #~ msgstr "Αδρανής" #~ msgid "Invisible" #~ msgstr "Αόρατος" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "Remind to take a break" #~ msgstr "Υπενθύμιση για διάλειμμα" #~ msgid "Extras" #~ msgstr "Έξτρα" #~ msgid "00" #~ msgstr "00" #~ msgid ":" #~ msgstr ":" #~ msgid "It seems to be uninstalled" #~ msgstr "Φαίνεται να έχει απεγκατασταθεί" #~ msgid "Extension is out of date" #~ msgstr "Το πρόσθετο είναι παλιό" #~ msgid "Upgrade" #~ msgstr "Αναβάθμιση" focustimerhq-FocusTimer-8581be2/po/eo.po000066400000000000000000001363011520625676500202440ustar00rootroot00000000000000# Esperanto translation for focus-timer # Copyright (c) 2019 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Sebastien Zurfluh , 2019. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 15:42+0100\n" "Last-Translator: Sebastien Zurfluh \n" "Language-Team: Esperanto\n" "Language: eo\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Tempilo" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Agordoj" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Starti/Halti" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Paŭzi/Daŭrigi" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Starti Pomodoron" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Starti" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Halti" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Paŭzi" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Daŭrigi" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Preterpasi" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Etendi nunan pomodoro aŭ rompo" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Vidigi agordojn" #: src/application.vala:203 msgid "Quit application" msgstr "Ĉesi programon" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Vidigi versiajn informojn kaj ĉesi" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%d sekundo pli" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Propraj agoj…" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Malsukcesis enŝalti entendaĵon" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Komando" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "" #: src/core/command.vala:515 msgid "Invalid command" msgstr "" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Agoj" #: src/core/event.vala:183 msgid "Countdown" msgstr "" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "" #: src/core/event.vala:189 msgid "Other" msgstr "" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Starti tempilon" #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "" #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "" #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "" #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "" #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "" #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" #: src/core/event.vala:333 msgid "Changed" msgstr "" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "" #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "" #: src/core/event.vala:350 msgid "Advanced" msgstr "" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "" #: src/core/event.vala:358 msgid "State Changed" msgstr "" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" #: src/core/event.vala:366 msgid "Rescheduled" msgstr "" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "" #: src/core/event.vala:374 msgid "Expired" msgstr "" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "" #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Konsideru paŭzon" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Prenu mallongan paŭzon" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Prenu longan paŭzon" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro tuje finiĝos" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Konsideru paŭzon" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Paŭzo tuje finiĝos" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 Minuto" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Preparu…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Tempilo de Pomodoro" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Paŭzo finiĝis" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Preterpasi paŭzon" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "" #: src/core/sounds.vala:112 msgid "File not found" msgstr "" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Halti" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Paŭzo" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Paŭzeto" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Paŭzego" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%d horo" msgstr[1] "%d horoj" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%d minuto" msgstr[1] "%d minutoj" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "" msgstr[1] "" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "" #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "" #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "" #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "" #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "" #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "" #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "" #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "" #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "" #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Pri" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Malsukcesis enŝalti entendaĵon" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Labortablo" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Paŭzata" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistikoj" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Ĉesi" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "" #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Komando" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Sébastien Zurfluh, 2019" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Paŭzo" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Daŭro de paŭzeto" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Tago" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Semajno" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Monato" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Neniu ankoraŭ videblas" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Hodiaŭ" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Hieraŭ" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Ĉi-tiu semajno" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Semajno" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Paŭze_to" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Paŭze_go" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "Paŭzo" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Daŭro de paŭzego" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "%d minuto" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Agordoj" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Pri" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Ĉesi" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Lanĉi kiel fona servo" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Estas tempo paŭzi" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Tempilo fenestro" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Starti" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Paŭzata" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Propraj agoj…" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Nuligi" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Namo" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Kaŭzantoj" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Komando" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Komando" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "Ago" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Propraj agoj…" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Elektu" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Daŭro de stato" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "+1 Minuto" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Stato" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Daŭro de stato" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Fulmklavo" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Malebligi" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Pomodoro tuje finiĝos" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Sciigoj" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Fulmklavo" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Propraj agoj…" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Pomodoro tuje finiĝos" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Paŭzo tuje finiĝos" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Sono de Tiktako" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Kloŝo" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Kloŝego" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tiktako de Horloĝo" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Laŭteco:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Elekti propran sonon" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Daŭro de pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Daŭro de paŭzeto" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Daŭro de paŭzego" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Starti Pomodoron" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Simpla temporganizilo" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Kaŝi aliajn sciigojn" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Etendi nunan pomodoro aŭ rompo" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Halti tempilon" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Montrilo por GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Montrilo por GNOME Shell" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Montrilo por GNOME Shell" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Montrilo por GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Montrilo por GNOME Shell" #, fuzzy #~ msgid "1 minute" #~ msgstr "%d minuto" #, fuzzy #~ msgid "2 minutes" #~ msgstr "%d minuto" #, fuzzy #~ msgid "3 minutes" #~ msgstr "%d minuto" #, fuzzy #~ msgid "5 minutes" #~ msgstr "%d minuto" #~ msgid "Timer Ticking" #~ msgstr "Tiktako de Tempilo" #~ msgid "timer;" #~ msgstr "tempilo;" #~ msgid "Start/Stop" #~ msgstr "Starti/Halti" #~ msgid "Pause/Resume" #~ msgstr "Paŭzi/Daŭrigi" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Saltu al pomodoro aŭ al paŭzo" #~ msgid "Reset current session" #~ msgstr "Restarigi nunan sesion" #~ msgid "A time management utility for GNOME" #~ msgstr "Organizilo de tempo por GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Ilo por GNOME kiu helpas organizi tempon laŭ la Pomodoro tekniko. Ĝi " #~ "intencas plibonigi la povo kaj koncentriĝo per mallongaj paŭzoj po ĉiu 25 " #~ "minutoj da laboro." #~ msgid "_Timer" #~ msgstr "_Tempilo" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Fulmklavo baskulante la tempilo. Enigu novan fulmklavon por ĝin ŝanĝi." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoroj antaŭ paŭzegoj" #~ msgid "Keyboard shortcut" #~ msgstr "Fulmklavo" #~ msgid "Screen notifications" #~ msgstr "Ekranaj sciigoj" #~ msgid "Wait for activity after a break" #~ msgstr "Ĝisatendi stimulon antaŭ finpaŭzi" #~ msgid "Plugins…" #~ msgstr "Kromprogramoj…" #~ msgid "Plugins" #~ msgstr "Kromprogramoj" #~ msgid "Back" #~ msgstr "Reen" #~ msgid "Complete a few sessions" #~ msgstr "Plenumi kelkajn seancojn" #~ msgid "Previous (Alt+Left)" #~ msgstr "Antaŭa (Alt+Maldekstra)" #~ msgid "Next (Alt+Right)" #~ msgstr "Sekva (Alt+Dekstra)" #~ msgid "Complete" #~ msgstr "Plenumi" #~ msgid "Enable" #~ msgstr "Ŝalti" #~ msgid "Add" #~ msgstr "Aldoni" #~ msgid "Remove" #~ msgstr "Eltiri" #~ msgid "Elapsed Time" #~ msgstr "Forpasanta Tempo" #~ msgid "Pause Timer" #~ msgstr "Paŭzigi tempilon" #~ msgid "Pause break" #~ msgstr "Paŭzigi" #~ msgid "Pause Pomodoro" #~ msgstr "Paŭzigi" #~ msgid "Resume break" #~ msgstr "Daŭrigu" #~ msgid "Resume Pomodoro" #~ msgstr "Daŭrigu" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minuto pli" #~ msgstr[1] "%d minutoj pli" #~ msgid "Report issue" #~ msgstr "Sciigi atentindaĵon" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Malsukcesis lanĉi la servon de %s" #~ msgid "Woodland Birds" #~ msgstr "Birdo de arbaro" #~ msgid "End of Break Sound" #~ msgstr "Sono por Paŭzfino" #~ msgid "Start of Break Sound" #~ msgstr "Sono por Paŭzoeko" #~ msgid "Off" #~ msgstr "Malŝatita" #~ msgid "Ticking sound" #~ msgstr "Tiktako" #~ msgid "Start of break sound" #~ msgstr "Sono por paŭzoeko" #~ msgid "End of break sound" #~ msgstr "Sono por paŭzfino" #~ msgid "About Pomodoro" #~ msgstr "Pri Pomodoro" #~ msgid "Focus on your task." #~ msgstr "Koncentriĝu al vian agadon." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Vi havas %d minuton" #~ msgstr[1] "Vi havas %d minutojn" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Vi havas %d sekundon" #~ msgstr[1] "Vi havas %d sekundojn" #~ msgid "Take a longer break" #~ msgstr "Daŭrigi paŭzon" #~ msgid "Lengthen it" #~ msgstr "Plidaŭrigi ĝin" #~ msgid "Shorten it" #~ msgstr "Maldaŭrigi ĝin" #~ msgid "Start pomodoro" #~ msgstr "Eki pomodoron" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Uzi \"%s\" kiel fulmklavo interferos kun tajpado. Provu aldoni kroman " #~ "klavon, kiel ekzemple Kontrol-, Alt- aŭ Ŝift-klavo." #~ msgid "Available" #~ msgstr "Disponebla" #~ msgid "Busy" #~ msgstr "Okupata" #~ msgid "Idle" #~ msgstr "Sencela" #~ msgid "Invisible" #~ msgstr "Nevidebla" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Statistikoj" #~ msgid "It seems to be uninstalled" #~ msgstr "Ŝajnas ĝi malinstaliĝas" #~ msgid "Extension is out of date" #~ msgstr "Etendaĵo malĝisdatas" #~ msgid "Upgrade" #~ msgstr "Ĝisdati" focustimerhq-FocusTimer-8581be2/po/es.po000066400000000000000000002033661520625676500202560ustar00rootroot00000000000000# Spanish translation for focus-timer # Copyright (c) 2012 - 2025 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # juanmah , 2012. # Juan Campos Zambrana , 2016. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 15:42+0100\n" "Last-Translator: Juan Campos Zambrana \n" "Language-Team: Spanish\n" "Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Un temporizador de productividad que te ayuda a trabajar de forma más eficaz " "dividiendo tu tiempo en sesiones de trabajo concentrado seguidas de breves " "descansos. Trabaja durante 25 minutos y luego tómate un descanso de 5 " "minutos para mantener la concentración y evitar el agotamiento." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Características principales:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Duración personalizable de las sesiones de trabajo y descansos" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Superposición de pantalla durante los descansos" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Temporizador" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Estadísticas diarias" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Estadísticas mensuales" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferencias" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Superposición de pantalla" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Comenzar o Detener" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Comenzar, Pausar o Continuar" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Comenzar un Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Comenzar" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Detener" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pausar" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Continuar" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Omitir" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Retroceder" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Extender el pomodoro o descanso actual" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Restablecer" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Mostrar las preferencias" #: src/application.vala:203 msgid "Quit application" msgstr "Salir de la aplicación" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Imprimir información de la versión y salir" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Traer al frente" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "Quedan %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "La acción personalizada \"%s\" ha fallado" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Tiempo de espera agotado" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Error al ejecutar el comando" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "El comando está vacío" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Comilla sin cerrar" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Comando no válido" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Variable desconocida \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Formato desconocido \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programa \"%s\" no encontrado" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Acciones" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Cuenta regresiva" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sesión" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Otros" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Temporizador iniciado." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Temporizador detenido manualmente." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "La cuenta regresiva se ha pausado manualmente. No se activa al bloquear la " "pantalla o suspender el sistema." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "La cuenta regresiva se ha reanudado manualmente." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "" "Se saltó al siguiente bloque de tiempo antes de terminar la cuenta regresiva." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Se ha usado la acción de retroceder. Añade una pausa en el tiempo pasado." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Sesión limpiada manualmente." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Finalizado" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "La cuenta regresiva ha terminado. Si se espera confirmación, la duración del " "bloque de tiempo aún puede alterarse." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Cambiado" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Activado por cualquier cambio relacionado con la cuenta regresiva." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Confirmar avance" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "" "Se requiere confirmación manual para iniciar el siguiente bloque de tiempo." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Avanzado" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Transición o salto al siguiente bloque de tiempo." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Estado cambiado" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" "Transición al siguiente bloque de tiempo o cuando un descanso cambia de " "etiqueta." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Reprogramado" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Activado cuando los bloques de tiempo programados han cambiado." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Expirado" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "Activado cuando la sesión está a punto de restablecerse por inactividad." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Toma un descanso" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Tomar un breve descanso" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Tomar un largo descanso" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "El Pomodoro está a punto de terminar" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Tómate un descanso" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "El descanso está a punto de terminar" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuto" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Prepárate…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "¡El Pomodoro ha terminado!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "¡El descanso ha terminado!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Confirma el inicio de un Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Confirma el inicio de un descanso…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Confirma el inicio de un descanso corto…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Confirma el inicio de un descanso largo…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Omitir descanso" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Error al inicializar la reproducción" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Archivo no encontrado" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Tipo de archivo no soportado" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Detenido" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Descanso" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Descanso corto" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Descanso largo" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u horas" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minutos" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segundo" msgstr[1] "%u segundos" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "La hora exacta del evento actual." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "La fase actual del ciclo Pomodoro. Valores posibles: stopped " "(detenido), pomodoro, break (descanso), short-break (descanso corto), long-break (descanso largo)." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Estado del bloque de tiempo actual. Valores posibles: scheduled " "(programado), in-progress (en curso), completed " "(completado), uncompleted (no completado)." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Indicador de si la cuenta regresiva ha comenzado." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Indicador de si la cuenta regresiva está pausada." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Indicador de si la cuenta regresiva ha finalizado." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Indicador de si el temporizador está contando activamente." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Duración de la cuenta regresiva actual." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Discrepancia entre el tiempo transcurrido y el tiempo real pasado." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Cantidad de tiempo invertido en la cuenta regresiva." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Cantidad de tiempo restante antes de que termine la cuenta regresiva." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Hora en que se inició la cuenta regresiva." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Extensión de GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "¡Obtén la mejor experiencia!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Activa la extensión de GNOME Shell para una integración perfecta" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Siempre a mano" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "" "Controla el temporizador desde la barra superior sin abrir la aplicación" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Menos distracciones" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Recordatorios de descanso refinados" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegante superposición a pantalla completa para que descansar sea más " "agradable" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "¿Listo para probarlo?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Instalar extensión" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Ahora no" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Algo salió mal" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Copiar al portapapeles" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Intentar de nuevo" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Abortar" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Se alcanzó el tiempo límite" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "No se permite instalar extensiones" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Error al descargar la extensión" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Superposición de pantalla" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Escritorio" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Extensión de GNOME Shell disponible" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Saber más" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Sin usar" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "¡Finalizado!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Estadísticas" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Salir" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Registro" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Registro vacío" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Las entradas aparecerán aquí una vez que inicies el temporizador." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Contexto" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Comando" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Salida" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Error" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Código de salida:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Tiempo de ejecución:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "juanmah , 2012\n" "Juan Campos Zambrana , 2016" #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Donar" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Descansos" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Interrupciones" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Ratio de descanso" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Día" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Semana" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mes" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "No hay nada que ver todavía" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "¡Completa unos cuantos Pomodoros para llenar esto!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Saltado %u día" msgstr[1] "Saltados %u días" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Saltada %u semana" msgstr[1] "Saltadas %u semanas" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Saltado %u mes" msgstr[1] "Saltados %u meses" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Hoy" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Ayer" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Esta semana" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Semana %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Semana %u de %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Descanso _corto" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Descanso _largo" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Descanso" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Abrir superposición de pantalla" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "La sesión ha expirado" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Descanso largo en %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Retroceder un minuto" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "Vista _compacta" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferencias" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Acerca de" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Salir" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Menú principal" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "¿Mantener el temporizador en marcha?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Puedes mantenerlo en segundo plano: las notificaciones y atajos de teclado " "seguirán funcionando." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Ejecutar en segundo plano" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "¡Es la hora de tomar un descanso!" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Ventana principal" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Preferir tema oscuro" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Preferir vista compacta" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Iniciado" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "Pausado" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Editar acción personalizada" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Cancelar" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Guardar" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nombre" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Activador" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Evento" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Ejecutar comando después de un evento." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Condición" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Asegura la ejecución de un segundo comando cuando ya no se cumpla la " "condición." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Eventos" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Añadir _evento" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtrar" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filtro" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Comando de terminal" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Comandos" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Comando si se cumple la condición" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Comando si no se cumple la condición" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Directorio de trabajo" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Usar subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Ejecuta el programa desde una subshell como sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Pasar datos de entrada" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "En lugar de pasar variables puedes procesar un objeto JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Esperar finalización" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Bloquea la ejecución de otros comandos hasta que el comando finalice." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Borrar acción" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Aún no se han especificado eventos." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Añadir acción personalizada" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Añadir" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Seleccionar directorio de trabajo" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Seleccionar" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Acción sin título" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Añadir condición" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Añadir grupo" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "Y" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "O" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Es" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "No es" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Igual a" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Mayor que" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Menor que" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Sí" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "No" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minutos" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Segundos" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Horas" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Seleccionar campo…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Estado" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "En ejecución" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Duración" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Insertar variable" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Formato" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Registro" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Mostrar registro de ejecución" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Ejecuta comandos de terminal automáticamente según eventos o condiciones. Saber más." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Establecer atajo" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Establecer" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Desactivado" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Pulsa Esc para cancelar o Retroceso para desactivar el atajo" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Los atajos globales permiten controlar la aplicación incluso si no está en " "pantalla. Funcionan siempre que la aplicación esté en segundo plano." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Abre la configuración para editar atajos globales" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Editar" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Introduce el nuevo atajo para iniciar o detener el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Introduce el nuevo atajo para iniciar/pausar/continuar el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Introduce el nuevo atajo para iniciar el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Introduce el nuevo atajo para detener el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Introduce el nuevo atajo para pausar el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Introduce el nuevo atajo para continuar el temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Introduce el nuevo atajo para omitir" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Retroceder un minuto" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Introduce el nuevo atajo para retroceder" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Introduce el nuevo atajo para enfocar la ventana" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Avisos" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "El tiempo se agota" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Notificar cuando el Pomodoro o el descanso esté por terminar." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Una notificación a pantalla completa para forzar el descanso." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Retraso de bloqueo" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Periodo de inactividad para bloquear la pantalla." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Retraso de reapertura" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Periodo de inactividad para reabrir la superposición tras descartarla." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Nunca" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notificaciones" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Sonidos" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Apariencia" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Atajos de teclado" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatización" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Los sonidos están desactivados" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Sonidos de alerta" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Sonido de Pomodoro finalizado" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Sonido de descanso finalizado" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Sonido de fondo" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Campana" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Campana ruidosa" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tic-tac del reloj" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Ninguno" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volumen:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Seleccionar sonido personalizado" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Duración del Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Duración de la pausa corta" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Duración de la pausa larga" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Número de ciclos" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Comportamiento" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pausar al bloquear la pantalla" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Confirmar inicio de un descanso" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Confirmar inicio de un Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Una sesión individual durará %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "El %u%% del tiempo se asignará a descansos." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "¿Aplicar cambios al Pomodoro en curso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "¿Aplicar cambios al descanso en curso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Aplicar" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Barra lateral" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Utilidad de gestión del tiempo" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Mantén la concentración tomando descansos frecuentes" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Notificaciones visuales y sonoras" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Seguimiento del tiempo y estadísticas" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integración con el escritorio GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Ejecutar comandos personalizados tras un Pomodoro o descanso" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Temporizador compacto" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Añadida traducción al tamil (gracias @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Añadida traducción al hebreo (gracias @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Soporte para GNOME Shell 49 (gracias @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Traducción al alemán actualizada (gracias @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Soporte para GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Dividir el tiempo transcurrido a medianoche" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Añadida traducción al telugu (gracias @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Soporte para GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Permitir descartar la superposición de pantalla mediante gestos al " #~ "reproducir vídeo" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Añadida traducción al georgiano (gracias @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Traducciones ajustadas en appdata (gracias @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Corrección: la notificación permanecía tras extender el Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Correcciones para GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Fin del soporte para GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Resumen de cambios en gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Soporte para GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Ajuste del script de construcción para meson 0.59.0 (gracias @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Deja que Pomodoro gestione las notificaciones mientras el " #~ "temporizador corre" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 segundos" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 segundos" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuto" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minutos" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minutos" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minutos" #~ msgid "Timer Ticking" #~ msgstr "Tic-tac del temporizador" #, fuzzy #~ msgid "Birds" #~ msgstr "Pájaros" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "temporizador;cronómetro;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Iniciar/Detener" #~ msgid "Pause/Resume" #~ msgstr "Pausar/Continuar" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Saltar a un pomodoro o a un descanso" #~ msgid "Reset current session" #~ msgstr "Restablecer sesión actual" #~ msgid "Run as background service" #~ msgstr "Ejecutar como servicio en segundo plano" #~ msgid "About Pomodoro" #~ msgstr "Acerca de Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Una sencilla utilidad de gestión del tiempo" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Detener" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indicador para GNOME Shell" #, fuzzy #~ msgid "_Install" #~ msgstr "Instalar" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Duración de la pausa larga" #~ msgid "A time management utility for GNOME" #~ msgstr "Una utilidad de gestión del tiempo para GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Una utilidad de GNOME que ayuda a administrar el tiempo según la Técnica " #~ "Pomodoro. Tiene la intención de mejorar la productividad y el enfoque " #~ "tomando descansos breves después de cada 25 minutos de trabajo." #~ msgid "Timer window" #~ msgstr "Ventana del temporizador" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indicador para GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indicador para GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indicador para GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indicador para GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Temporizador" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Atajo de teclado para conmutar el temporizador. Entre un nuevo atajo para " #~ "cambiarlo." #~ msgid "Disable" #~ msgstr "Desactivar" #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoros hasta un largo descanso" #~ msgid "Keyboard shortcut" #~ msgstr "Atajo de teclado" #~ msgid "Screen notifications" #~ msgstr "Notificaciones en pantalla" #~ msgid "Wait for activity after a break" #~ msgstr "Esperar actividad después de un descanso" #~ msgid "Plugins…" #~ msgstr "Plugins…" #~ msgid "Plugins" #~ msgstr "Plugins" #~ msgid "Back" #~ msgstr "AtraÌs" #~ msgid "Complete a few sessions" #~ msgstr "Completa algunas sesiones" #~ msgid "Previous (Alt+Left)" #~ msgstr "Anterior (Alt+Izquierda)" #~ msgid "Next (Alt+Right)" #~ msgstr "Siguiente (Alt+Derecha)" #~ msgid "Complete" #~ msgstr "Completo" #~ msgid "Enable" #~ msgstr "Activar" #~ msgid "Add" #~ msgstr "AnÌadir" #~ msgid "Remove" #~ msgstr "Quitar" #~ msgid "Elapsed Time" #~ msgstr "Tiempo transcurrido" #~ msgid "Pause Timer" #~ msgstr "Temporizador de pausa" #~ msgid "Pause break" #~ msgstr "Pausa" #~ msgid "Pause Pomodoro" #~ msgstr "Pausa" #~ msgid "Resume break" #~ msgstr "Continuar" #~ msgid "Resume Pomodoro" #~ msgstr "Continuar" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minuto restante" #~ msgstr[1] "%d minutos restante" #~ msgid "Report issue" #~ msgstr "Informar problema" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Error al ejecutar el servicio %s" #~ msgid "Woodland Birds" #~ msgstr "Pájaros" #~ msgid "End of Break Sound" #~ msgstr "Sonido del fin del descanso" #~ msgid "Start of Break Sound" #~ msgstr "Sonido del comienzo del descanso" #~ msgid "Off" #~ msgstr "Apagado" #~ msgid "Ticking sound" #~ msgstr "Sonido de tictac" #~ msgid "Start of break sound" #~ msgstr "Sonido del comienzo del descanso" #~ msgid "End of break sound" #~ msgstr "Sonido del fin del descanso" #~ msgid "Focus on your task." #~ msgstr "Céntrate en tu tarea." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Tienes %d minuto" #~ msgstr[1] "Tienes %d minutos" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Queda %d segundo" #~ msgstr[1] "Quedan %d segundos" #~ msgid "Take a longer break" #~ msgstr "Toma un descanso más largo" #~ msgid "Lengthen it" #~ msgstr "Alárgalo" #~ msgid "Shorten it" #~ msgstr "Acórtalo" #~ msgid "Start pomodoro" #~ msgstr "Comenzar un nuevo pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "El uso de \"%s\" como acceso directo interferirá con la escritura. " #~ "Intente agregar otra tecla, como Control, Alt o Shift." #~ msgid "Available" #~ msgstr "Disponible" #~ msgid "Busy" #~ msgstr "Ocupado" #~ msgid "Idle" #~ msgstr "Inactivo" #~ msgid "Invisible" #~ msgstr "Invisible" #~ msgid "A new pomodoro is starting" #~ msgstr "Comenzando un nuevo pomodoro" #~ msgid "Take a break!" #~ msgstr "¡Tómese un descanso!" #~ msgid "Shorten the break" #~ msgstr "Acortar el descanso" #~ msgid "Lengthen the break" #~ msgstr "Alargar el descanso" #, javascript-format #~ msgid "You have %d minute left" #~ msgid_plural "You have %d minutes left" #~ msgstr[0] "Queda %d minuto" #~ msgstr[1] "Quedan %d minutos" #~ msgid "Hey, you're missing out on a break" #~ msgstr "¡Se le está pasando el descanso!" #~ msgid "Could not run pomodoro" #~ msgstr "No se pudo ejecutar el pomodoro" #~ msgid "Looks like gnome-pomodoro is not installed" #~ msgstr "Parece que gnome-pomodoro no está instalado" #~ msgid "" #~ "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; either version 3 of the License, or (at your " #~ "option) any later version." #~ msgstr "" #~ "Este programa es software libre: puede redistribuirlo y/o modificarlo " #~ "bajo los términos de la Licencia General Pública de GNU publicada por la " #~ "Free Software Foundation, ya sea la versión 3 de la Licencia, o (a su " #~ "elección) cualquier versión posterior." #~ msgid "Remind to take a break" #~ msgstr "Recordatorio para tomar un descanso" #~ msgid "Select sound for pomodoro start" #~ msgstr "Selecciona un sonido para el inicio de un pomodoro" #~ msgid "Presence" #~ msgstr "Presencia" #~ msgid "Postpone pomodoro when idle" #~ msgstr "Posponer pomodoro cuando esté ausente" #~ msgid "Status during pomodoro" #~ msgstr "Estado durante el pomodoro" #~ msgid "Status during break" #~ msgstr "Estado durante la pausa" #~ msgid "" #~ "System notifications including chat messages won't show up during " #~ "pomodoro." #~ msgstr "" #~ "Las notificaciones del sistema (incluyendo mensajes de chat) se " #~ "desactivarán durante el pomodoro." #~ msgid "" #~ "System notifications including chat messages won't show up during break." #~ msgstr "" #~ "Las notificaciones del sistema (incluyendo mensajes de chat) se " #~ "desactivarán durante el descanso." #~ msgid "System notifications including chat messages won't show up." #~ msgstr "" #~ "Las notificaciones del sistema (incluyendo mensajes de chat) se " #~ "desactivarán." #~ msgid "OK" #~ msgstr "Aceptar" #, c-format #~ msgid "" #~ "The shortcut \"%s\" cannot be used because it will become impossible to " #~ "type using this key.\n" #~ "Please try with a key such as Control, Alt or Shift at the same time." #~ msgstr "" #~ "El atajo de teclado '%s' no puede usarse porque sería imposible escribir " #~ "usando esta tecla.\n" #~ "Por favor, pruebe a combinarlo con una tecla como Ctrl, Alt o Mayús." #~ msgid "_No sound" #~ msgstr "_Silenciar" #~ msgid "_Open" #~ msgstr "_Abrir" #~ msgid "All files" #~ msgstr "Todos los archivos" #~ msgid "Supported audio files" #~ msgstr "Ficheros de audio permitidos" #~ msgid "Manage your time and tasks" #~ msgstr "Gestione su tiempo y tareas" #~ msgid "time;timer;tasks;manage;organize;" #~ msgstr "tiempo;cronómetro;tareas;gestionar;organizar;" #~ msgid "Pomodoros until long break" #~ msgstr "Pomodoros hasta un largo descanso" #~ msgid "Presence during pomodoro" #~ msgstr "Estado durante el pomodoro" #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Tienes %d minuto hasta el próximo pomodoro" #~ msgstr[1] "Tienes %d minutos hasta el próximo pomodoro" #~ msgid "Presence During Pomodoro" #~ msgstr "Estado durante el pomodoro" #~ msgid "Reset Counts and Timer" #~ msgstr "Reiniciar el contador y el temporizador" #~ msgid "Away From Desk" #~ msgstr "Ausente del escritorio" #~ msgid "Control Presence Status" #~ msgstr "Control del estado de presencia" #~ msgid "%d Completed Session" #~ msgid_plural "%d Completed Sessions" #~ msgstr[0] "%d sesión completada" #~ msgstr[1] "%d sesiones completadas" #~ msgid "Pause finished." #~ msgstr "El descanso ha finalizado." #~ msgid "Time in seconds you are supposed to be working." #~ msgstr "Tiempo en segundos que se supone que son de trabajo." #~ msgid "Time in seconds you are supposed to have a short break." #~ msgstr "Tiempo en segundos que se supone que son de pausa corta." #~ msgid "Long pause duration" #~ msgstr "Duración de la pausa larga" #~ msgid "Time in seconds you are supposed to have a longer break." #~ msgstr "Tiempo en segundos que se supone que son de pausa larga." #~ msgid "Whether to show a notification dialog when pause starts." #~ msgstr "¿Mostrar un diálogo de notificación al comenzar un descanso?" #~ msgid "Disable flexible breaks" #~ msgstr "Desactivar pausas flexibles" #~ msgid "Whether you are not using a computer to work." #~ msgstr "Si no se está usando un ordenador para trabajar." #~ msgid "Change user presence status to busy" #~ msgstr "Cambiar el estado de presencia del usuario a ocupado" #~ msgid "Whether to change user and IM presence to busy." #~ msgstr "Si se cambia el usuario y la presencia de chat a ocupado." #~ msgid "Whether to play a sound to notify of events." #~ msgstr "Si se reproduce un sonido para notificar los eventos." #~ msgid "Notification sound file" #~ msgstr "Archivo de sonido de la notificación" #~ msgid "Restore timer state" #~ msgstr "Restaurar el estado del temporizador" #~ msgid "Whether to restore state on startup." #~ msgstr "Si se restaura el estado en el inicio." #~ msgid "Number of completed sessions since long break" #~ msgstr "Número de sesiones completadas después de una pausa larga" #~ msgid "Saved timer state" #~ msgstr "Estado del temporizador guardado" #~ msgid "Time of saved state" #~ msgstr "Hora del estado guardado" #~ msgid "Show Dialog Messages" #~ msgstr "Mostrar mensajes de diálogo" #~ msgid "Click to reset session counts to zero" #~ msgstr "Pulse para reiniciar el contador de la sesión a cero" #~ msgid "Set optimal settings for doing paperwork" #~ msgstr "Ajustes óptimos para hacer papeleo" #~ msgid "Show a dialog message at the end of pomodoro session" #~ msgstr "Mostrar un mensaje de diálogo al final de la sesión de pomodoro" #~ msgid "Play a sound at start of pomodoro session" #~ msgstr "Reproducir un sonido al comenzar la sesión de pomodoro" #~ msgid "Hide" #~ msgstr "Ocultar" #~ msgid "Timer toggle key" #~ msgstr "Tecla de conmutación del temporizador" focustimerhq-FocusTimer-8581be2/po/fi.po000066400000000000000000001672061520625676500202470ustar00rootroot00000000000000# Finnish translation for focus-timer # Copyright (c) 2020 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Tuomas Jaakola , 2020. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-24 16:00+0100\n" "Last-Translator: Tuomas Jaakola \n" "Language-Team: Finnish\n" "Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Tuottavuusajastin, joka auttaa sinua työskentelemään tehokkaammin jakamalla " "aikasi keskittyneisiin työjaksoihin ja lyhyisiin taukoihin. Työskentele 25 " "minuuttia ja pidä sitten 5 minuutin tauko ylläpitääksesi keskittymistä ja " "ehkäistäksesi uupumusta." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Keskeiset ominaisuudet:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Muokattavat työjaksojen ja taukojen pituudet" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Koko näytön peitto taukojen aikana" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Ajastin" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Päivittäiset tilastot" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Kuukausitilastot" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Asetukset" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Näytön peitto" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Käynnistä tai pysäytä" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Käynnistä, keskeytä tai jatka" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Käynnistä Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Käynnistä" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Pysäytä" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Keskeytä" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Jatka" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Ohita" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Kelaa taaksepäin" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Pidennä nykyistä pomodoroa tai taukoa" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Nollaa" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Näytä asetukset" #: src/application.vala:203 msgid "Quit application" msgstr "Lopeta sovellus" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Näytä versiotiedot ja poistu" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Tuo etualalle" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s jäljellä" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Oma toiminto \"%s\" epäonnistui" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Aikakatkaisu saavutettu" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Komennon suorittaminen epäonnistui" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Komento on tyhjä" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Sulkematon lainausmerkki" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Virheellinen komento" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Tuntematon muuttuja \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Tuntematon muoto \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Ohjelmaa \"%s\" ei löydy" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Toiminnot" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Laskuri" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Istunto" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Muu" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Ajastin käynnistetty." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Ajastin pysäytetty manuaalisesti." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Laskenta on keskeytetty manuaalisesti. Ei aktivoidu näytön lukituksen tai " "valmiustilan yhteydessä." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Laskentaa on jatkettu manuaalisesti." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Hypätty seuraavaan aikajaksoon ennen laskennan päättymistä." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Kelaustoimintoa on käytetty. Se lisää tauon menneisyyteen." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Istunto tyhjennetty manuaalisesti." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Valmis" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Laskenta on päättynyt. Jos vahvistusta odotetaan, aikajakson kestoa voidaan " "vielä muuttaa." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Muuttunut" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Aktivoituu mistä tahansa laskentaan liittyvästä muutoksesta." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Vahvista siirtyminen" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Seuraavan jakson aloittaminen vaatii manuaalisen vahvistuksen." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Siirrytty eteenpäin" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Siirrytty tai hypätty seuraavaan aikajaksoon." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Tila muuttunut" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Siirrytty seuraavaan jaksoon tai kun tauko nimetään uudelleen." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Uudelleenajoitettu" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Aktivoituu, kun ajoitetut aikajaksot ovat muuttuneet." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Vanhentunut" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Aktivoituu, kun istunto nollataan pian käyttämättömyyden vuoksi." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Pidä tauko" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Pidä lyhyt tauko" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Pidä pitkä tauko" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro on päättymässä" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Pidä tauko" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Tauko on päättymässä" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuutti" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Valmistaudu…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Pomodoro on ohi!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Tauko on ohi!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Vahvista pomodoron aloitus…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Vahvista tauon aloitus…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Vahvista lyhyen tauon aloitus…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Vahvista pitkän tauon aloitus…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Ohita tauko" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Äänentoiston alustaminen epäonnistui" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Tiedostoa ei löydy" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Tiedostotyyppi ei ole tuettu" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Pysäytetty" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Tauko" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Lyhyt tauko" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Pitkä tauko" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u t" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u min" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u tunti" msgstr[1] "%u tuntia" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuutti" msgstr[1] "%u minuuttia" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunti" msgstr[1] "%u sekuntia" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Nykyisen tapahtuman tarkka aika." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Pomodoro-syklin nykyinen vaihe. Mahdolliset arvot: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Nykyisen aikajakson tila. Mahdolliset arvot: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Lippu, joka kertoo onko laskenta alkanut." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Lippu, joka kertoo onko laskenta keskeytetty." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Lippu, joka kertoo onko laskenta päättynyt." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Lippu, joka kertoo laskeeko ajastin parhaillaan aikaa." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Nykyisen laskennan kesto." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Ero kuluneen ja todellisen ajan välillä." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Laskentaan käytetty aika." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Jäljellä oleva aika ennen laskennan päättymistä." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Aika, jolloin laskenta alkoi." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell -laajennus" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Hanki paras käyttökokemus!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Ota GNOME Shell -laajennus käyttöön saumatonta työpöytäintegraatiota " "varten" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Aina ulottuvillasi" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Hallitse ajastinta suoraan yläpalkista avaamatta sovellusta" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Vähemmän häiriötekijöitä" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Tyylikkäät taukumuistutukset" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Tyylikäs koko näytön peitto, joka tekee taukojen pitämisestä miellyttävämpää" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Oletko valmis kokeilemaan?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Asenna laajennus" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Ei nyt" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Jokin meni vikaan" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Kopioi leikepöydälle" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Yritä uudelleen" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Keskeytä" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Aikakatkaisu saavutettu" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Laajennusten asennus ei ole sallittua" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Laajennuksen lataaminen epäonnistui" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Näytön peitto" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Työpöytä" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell -laajennus saatavilla" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Lue lisää" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Käyttämätön" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Valmis!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Tilastot" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Lopeta" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Loki" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Tyhjä loki" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Merkinnät näkyvät tässä, kun käynnistät ajastimen." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Konteksti" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Komento" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Tuloste" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Virhe" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Paluukoodi:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Suoritusaika:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Tuomas Jaakola " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Lahjoita" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Tauot" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Keskeytykset" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Taukojen suhde" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Päivä" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Viikko" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Kuukausi" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Täällä ei ole vielä mitään" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Suorita muutama pomodoro täyttääksesi tämän!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Hypätty %u päivän yli" msgstr[1] "Hypätty %u päivän yli" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Hypätty %u viikon yli" msgstr[1] "Hypätty %u viikon yli" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Hypätty %u kuukauden yli" msgstr[1] "Hypätty %u kuukauden yli" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Tänään" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Eilen" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Tällä viikolla" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Viikko %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Viikko %u / %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Lyhyt tauko" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Pitkä _tauko" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Tauko" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Avaa näytön peitto" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Istunto on vanhentunut" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Pitkä tauko alkaa %s kuluttua" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Kelaa minuutti taaksepäin" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Pieni näkymä" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Asetukset" #: src/ui/main/window.ui:19 msgid "_About" msgstr "T_ietoa" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Lopeta" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Päävalikko" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Pidetäänkö ajastin käynnissä?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Voit pitää sen käynnissä taustalla — ilmoitukset ja pikanäppäimet toimivat " "edelleen." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Suorita taustalla" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "On aika pitää taukoa" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Pääikkuna" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Suosi tummaa teemaa" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Suosi pientä näkymää" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Käynnistetty" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Keskeytetty" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Muokkaa omaa toimintoa" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Peru" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Tallenna" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nimi" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Laukaisin" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Tapahtuma" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Suorita komento tapahtuman jälkeen." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Ehto" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "Varmista toisen komennon suoritus, kun ehto ei enää täyty." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Tapahtumat" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Lisää _tapahtuma" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Suodata" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Suodatin" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell-komento" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Komennot" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Komento, kun ehto täyttyy" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Komento, kun ehto ei täyty" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Työhakemisto" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Käytä alikuorta" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Suorita ohjelma alikuoresta, kuten sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Välitä syötetiedot" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Muuttujien välittämisen sijaan voit käsitellä JSON-objektia." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Odota valmistumista" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Estä muiden komentojen suoritus, kunnes komento on valmis." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Poista toiminto" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Tapahtumia ei ole vielä määritetty." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Lisää oma toiminto" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Lisää" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Valitse työhakemisto" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Valitse" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Nimetön toiminto" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Lisää ehto" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Lisää ryhmä" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "JA" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "TAI" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "On" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Ei ole" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "On yhtä suuri kuin" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "On suurempi kuin" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "On pienempi kuin" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Kyllä" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Ei" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuuttia" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Sekuntia" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Tuntia" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Valitse kenttä…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Tila" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Käynnissä" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Kesto" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Lisää muuttuja" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Muoto" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Loki" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Näytä suoritusloki" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Suorita shell-komentoja automaattisesti ajastimen tapahtumien tai ehtojen " "perusteella. Lue lisää." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Aseta pikanäppäin" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Aseta" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Pois käytöstä" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Paina Esc peruaksesi tai Askelpalautin poistaaksesi " "pikanäppäimen käytöstä" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Yleisten pikanäppäinten avulla voit hallita sovellusta, vaikka se ei olisi " "näytöllä. Ne toimivat niin kauan kuin sovellus on käynnissä taustalla." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Avaa sovelluksen asetukset yleisten pikanäppäinten muokkaamiseksi" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Muokkaa" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Anna uusi pikanäppäin ajastimen käynnistämiseen tai pysäyttämiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Anna uusi pikanäppäin ajastimen käynnistämiseen, keskeyttämiseen tai " "jatkamiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Anna uusi pikanäppäin ajastimen käynnistämiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Anna uusi pikanäppäin ajastimen pysäyttämiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Anna uusi pikanäppäin ajastimen keskeyttämiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Anna uusi pikanäppäin ajastimen jatkamiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Anna uusi pikanäppäin ohittamiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Kelaa yksi minuutti taaksepäin" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Anna uusi pikanäppäin kelaamiseen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Anna uusi pikanäppäin ikkunan tuomiseen etualalle" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Ilmoitukset" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Aika loppumassa" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Ilmoita, kun pomodoro tai tauko on päättymässä." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "" "Koko näytön ilmoitus, joka on tarkoitettu pakottamaan tauon pitämiseen." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Lukitusviive" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Käyttämättömyyden kesto ennen näytön lukitsemista." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Uudelleenavausviive" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Käyttämättömyyden kesto peittokuvan uudelleenavaamiseen sen sulkemisen " "jälkeen." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Ei koskaan" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Ilmoitukset" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Äänet" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Ulkoasu" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Pikanäppäimet" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automaatio" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Äänet on poistettu käytöstä" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Hälytysäänet" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Pomodoron päättymisääni" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Tauon päättymisääni" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Taustaääni" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Kello" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Iso kello" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Kellon tikitys" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Ei mitään" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Äänenvoimakkuus:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Valitse oma ääni" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Pomodoron kesto" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Lyhyen tauon kesto" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Pitkän tauon kesto" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Syklien määrä" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Toiminta" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Keskeytä lukitsemalla näyttö" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Vahvista tauon aloitus" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Vahvista pomodoron aloitus" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Yksi istunto kestää %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% ajasta käytetään taukoihin." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Otetaanko muutokset käyttöön meneillään olevaan pomodoroon?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Otetaanko muutokset käyttöön meneillään olevaan taukoon?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Käytä" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Sivupalkki" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Ajanhallintatyökalu" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Säilytä keskittymiskyky säännöllisillä tauoilla" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Visuaaliset ja äänimerkit" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Ajanseuranta ja tilastot" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "GNOME-työpöytäintegraatio" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Suorita omia komentoja pomodoron tai tauon jälkeen" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Pienikokoinen ajastin" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.28.1 -versiossa" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Lisätty tamilinkielinen käännös (kiitos @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Lisätty hepreankielinen käännös (kiitos @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.28.0 -versiossa" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Tuki GNOME Shell 49 -versiolle (kiitos @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Päivitetty saksankielinen käännös (kiitos @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.27.0 -versiossa" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Tuki GNOME Shell 48 -versiolle" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Keskiyön ylittävän ajan jakaminen" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Lisätty telugunkielinen käännös (kiitos @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.26.0 -versiossa" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Tuki GNOME Shell 47 -versiolle" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Salli näytön peiton ohittaminen eleellä, kun videota toistetaan" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Lisätty georgiankielinen käännös (kiitos @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Muokattu appdata-käännöksiä (kiitos @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.25.2 -versiossa" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Korjattu ilmoituksen jääminen näkyviin pomodoron jatkamisen jälkeen" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.25.1 -versiossa" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Korjauksia GNOME Shell 46 -versiolle" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Poistettu tuki GNOME Shell 45 -versiolle" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Yhteenveto muutoksista gnome-pomodoro 0.25.0 -versiossa" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Tuki GNOME Shell 46 -versiolle" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Käännösskriptin päivitys meson 0.59.0 -versiolle (kiitos @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Anna Pomodoron hallita järjestelmän ilmoituksia ajastimen ollessa " #~ "käynnissä" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 sekuntia" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 sekuntia" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuutti" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minuuttia" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minuuttia" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minuuttia" #~ msgid "Timer Ticking" #~ msgstr "Ajastimen tikitys" #, fuzzy #~ msgid "Birds" #~ msgstr "Linnut" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "ajastin;timer;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Käynnistä/Pysäytä" #~ msgid "Pause/Resume" #~ msgstr "Keskeytä/Jatka" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Hyppää pomodoroon tai taukoon" #~ msgid "Reset current session" #~ msgstr "Nollaa nykyinen istunto" #~ msgid "Run as background service" #~ msgstr "Suorita taustapalveluna" #~ msgid "About Pomodoro" #~ msgstr "Tietoa Pomodorosta" #~ msgid "A simple time management utility" #~ msgstr "Yksinkertainen ajanhallintatyökalu" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Pysäytä" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Liitännäistä ei voitu ottaa käyttöön" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Pitkän tauon kesto" #~ msgid "A time management utility for GNOME" #~ msgstr "Ajanhallintatyökalu GNOMElle" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "GNOME-työkalu, joka käyttää Pomodoro-tekniikkaa ajanhallintaan. Se tähtää " #~ "tuottavuuden ja keskittymisen parantamiseen pitämällä pieniä taukoja 25 " #~ "minuutin työjaksojen jälkeen." #~ msgid "Timer window" #~ msgstr "Ajastimen ikkuna" #~ msgid "Indicator for GNOME Shell" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "GNOME Shellin indikaattori" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "GNOME Shellin indikaattori" #~ msgid "_Timer" #~ msgstr "_Ajastin" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "Pikanäppäin ajastimen käyttöön. Vaihda painamalla näppäintä." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoroja ennen pitkää taukoa" #~ msgid "Keyboard shortcut" #~ msgstr "Pikanäppäin" #~ msgid "Screen notifications" #~ msgstr "Näytön ilmoitukset" #~ msgid "Wait for activity after a break" #~ msgstr "Odota aktiviteettia tauon jälkeen" #~ msgid "Plugins…" #~ msgstr "Liitännäiset…" #~ msgid "Plugins" #~ msgstr "Liitännäiset" #~ msgid "Back" #~ msgstr "Takaisin" #~ msgid "Complete a few sessions" #~ msgstr "Vie loppuun muutama jakso" #~ msgid "Previous (Alt+Left)" #~ msgstr "Edellinen (Alt+Vasen)" #~ msgid "Next (Alt+Right)" #~ msgstr "Seuraava (Alt+Oikea)" #~ msgid "Complete" #~ msgstr "Valmis" #~ msgid "Enable" #~ msgstr "Ota käyttöön" #~ msgid "Add" #~ msgstr "Lisää" #~ msgid "Remove" #~ msgstr "Poista" #~ msgid "Elapsed Time" #~ msgstr "Kulunut aika" #~ msgid "Pause Timer" #~ msgstr "Keskeytä ajastin" #~ msgid "Pause break" #~ msgstr "Keskeytä" #~ msgid "Pause Pomodoro" #~ msgstr "Keskeytä" #~ msgid "Resume break" #~ msgstr "Jatka" #~ msgid "Resume Pomodoro" #~ msgstr "Jatka" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minuutti jäljellä" #~ msgstr[1] "%d minuuttia jäljellä" #~ msgid "Report issue" #~ msgstr "Ilmoita ongelmasta" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Palvelua %s ei voitu suorittaa" #~ msgid "Woodland Birds" #~ msgstr "Metsän linnut" #~ msgid "End of Break Sound" #~ msgstr "Tauon päättymisääni" #~ msgid "Start of Break Sound" #~ msgstr "Tauon aloitusääni" #~ msgid "Off" #~ msgstr "Pois" #~ msgid "Ticking sound" #~ msgstr "Tikitysääni" #~ msgid "Start of break sound" #~ msgstr "Tauon aloitusääni" #~ msgid "End of break sound" #~ msgstr "Tauon päättymisääni" #~ msgid "Focus on your task." #~ msgstr "Keskity tehtävääsi." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Sinulla on %d minuutti" #~ msgstr[1] "Sinulla on %d minuuttia" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Sinulla on %d sekunti" #~ msgstr[1] "Sinulla on %d sekuntia" #~ msgid "Take a longer break" #~ msgstr "Pidä pitkä tauko" #~ msgid "Lengthen it" #~ msgstr "Pidennä sitä" #~ msgid "Shorten it" #~ msgstr "Lyhennä sitä" #~ msgid "Start pomodoro" #~ msgstr "Käynnistä pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Näppäimen \"%s\" käyttö häiritsee kirjoittamista. Käytä jotain toista " #~ "näppäintä, esimerkiksi Control, Alt tai Shift." #~ msgid "Available" #~ msgstr "Saatavilla" #~ msgid "Busy" #~ msgstr "Varattu" #~ msgid "Idle" #~ msgstr "Jouten" #~ msgid "Invisible" #~ msgstr "Näkymätön" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Tilastot" #~ msgid "It seems to be uninstalled" #~ msgstr "Ilmeisesti se on poistettu" #~ msgid "Extension is out of date" #~ msgstr "Liitännäinen on vanhentunut" #~ msgid "Upgrade" #~ msgstr "Päivitä" focustimerhq-FocusTimer-8581be2/po/focus-timer.pot000066400000000000000000001203421520625676500222600ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the focus-timer package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: focus-timer\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "" #: src/application.vala:203 msgid "Quit application" msgstr "" #: src/application.vala:206 msgid "Print version information and exit" msgstr "" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "" #: src/core/command.vala:515 msgid "Invalid command" msgstr "" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "" #: src/core/event.vala:183 msgid "Countdown" msgstr "" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "" #: src/core/event.vala:189 msgid "Other" msgstr "" #: src/core/event.vala:269 msgid "Started the timer." msgstr "" #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "" #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "" #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "" #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "" #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "" #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" #: src/core/event.vala:333 msgid "Changed" msgstr "" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "" #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "" #: src/core/event.vala:350 msgid "Advanced" msgstr "" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "" #: src/core/event.vala:358 msgid "State Changed" msgstr "" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" #: src/core/event.vala:366 msgid "Rescheduled" msgstr "" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "" #: src/core/event.vala:374 msgid "Expired" msgstr "" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "" #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "" #: src/core/sounds.vala:112 msgid "File not found" msgstr "" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "" msgstr[1] "" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "" msgstr[1] "" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "" msgstr[1] "" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "" #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "" #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "" #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "" #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "" #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "" #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "" #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "" #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "" #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "" #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "" msgstr[1] "" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "" #: src/ui/main/window.ui:19 msgid "_About" msgstr "" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "" focustimerhq-FocusTimer-8581be2/po/fr.po000066400000000000000000002063161520625676500202540ustar00rootroot00000000000000# French translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Benjamin Danon , 2012. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 11:39+0100\n" "Last-Translator: Bastien Traverse \n" "Language-Team: French\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n>1;\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Un minuteur de productivité qui vous aide à travailler plus efficacement en " "divisant votre temps en sessions de travail concentré suivies de courtes " "pauses. Travaillez pendant 25 minutes, puis prenez une pause de 5 minutes " "pour maintenir votre concentration et éviter l'épuisement." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Caractéristiques principales :" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Durées des sessions de travail et des pauses personnalisables" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Superposition d'écran pendant les pauses" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Minuteur" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Statistiques quotidiennes" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Statistiques mensuelles" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Préférences" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Superposition d'écran" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Commencer ou Arrêter" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Commencer, Interrompre ou Continuer" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Démarrer Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Commencer" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Arrêter" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Interrompre" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Continuer" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Sauter" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Rembobiner" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 #, fuzzy msgid "Extend current pomodoro or break" msgstr "Prolonger le pomodoro ou la pause actuelle" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Réinitialiser" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Afficher les préférences" #: src/application.vala:203 msgid "Quit application" msgstr "Quitter l'application" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Imprimer les informations de version et quitter" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Mettre au premier plan" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s restant" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "L'action personnalisée \"%s\" a échoué" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Délai d'attente atteint" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Échec de l'exécution de la commande" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "La commande est vide" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Guillemet non fermé" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Commande invalide" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Variable inconnue \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Format inconnu \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programme \"%s\" non trouvé" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Actions" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Compte à rebours" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Session" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Autre" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Le minuteur a démarré." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Le minuteur a été arrêté manuellement." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Le compte à rebours a été mis en pause manuellement. Ne se déclenche pas " "lors du verrouillage de l'écran ou de la mise en veille du système." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Le compte à rebours a été repris manuellement." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Passage au bloc suivant avant la fin du compte à rebours." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "L'action rembobiner a été utilisée. Elle ajoute une pause dans le passé." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "La session a été effacée manuellement." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Terminé" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Le compte à rebours est terminé. En attente de confirmation, la durée du " "bloc peut encore être modifiée." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Modifié" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Déclenché lors de tout changement lié au compte à rebours." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Confirmer l'avancement" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Une confirmation manuelle est requise pour démarrer le bloc suivant." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Avancé" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Passage ou saut vers le bloc suivant." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "État modifié" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Passage au bloc suivant ou renommage d'une pause." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Reprogrammé" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Déclenché lorsque les blocs de temps prévus ont changé." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Expiré" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "Déclenché quand la session va être réinitialisée pour cause d'inactivité." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Faites une pause" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Faites une courte pause" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Faire une longue pause" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro est sur le point de se terminer" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Faites une pause" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "La pause est sur le point de se terminer" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minute" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Sois prêt…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Le Pomodoro est terminé !" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "La pause est terminée" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Confirmez le début d'un Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Confirmez le début d'une pause…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Confirmez le début d'une courte pause…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Confirmez le début d'une longue pause…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Sauter la pause" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Échec de l'initialisation de la lecture" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Fichier non trouvé" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Type de fichier non supporté" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Arrêté" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pause" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Courte pause" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Longue pause" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u heure" msgstr[1] "%u heures" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minute" msgstr[1] "%u minutes" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u seconde" msgstr[1] "%u secondes" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "L'heure exacte de l'événement actuel." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "La phase actuelle du cycle Pomodoro. Valeurs possibles : stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Statut du bloc de temps actuel. Valeurs possibles : scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Un indicateur signalant si le compte à rebours a commencé." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Un indicateur signalant si le compte à rebours est en pause." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Un indicateur signalant si le compte à rebours est terminé." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Un indicateur signalant si le minuteur décompte activement." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Durée du compte à rebours actuel." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Différence entre le temps écoulé et le temps passé." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "La quantité de temps passé sur le compte à rebours." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "La quantité de temps restant avant la fin du compte à rebours." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "L'heure à laquelle le compte à rebours a commencé." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Extension GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Profitez de la meilleure expérience !" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Activez l'extension GNOME Shell pour une intégration parfaite au " "bureau" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Toujours à portée de main" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "" "Contrôlez le minuteur directement depuis la barre supérieure sans ouvrir " "l'application" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Moins de distractions" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Rappels de pause raffinés" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Une élégante superposition plein écran qui rend la pause plus agréable" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Prêt à essayer ?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Installer l'extension" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Pas maintenant" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Une erreur s'est produite" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Copier dans le presse-papiers" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Réessayer" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "À propos" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Délai expiré" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "L'installation d'extensions n'est pas autorisée" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Échec du téléchargement de l'extension" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "Icône" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "Texte" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Superposition d'écran" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Bureau" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Extension GNOME Shell disponible" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "En savoir plus" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Inutilisé" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Terminé !" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistiques" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Quitter" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Journal" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Journal vide" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "" "Les entrées apparaîtront ici une fois que vous aurez démarré le minuteur." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Contexte" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Commande" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Sortie" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Erreur" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Code de sortie :" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Temps d'exécution :" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Bastien Traverse " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Faire un don" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pauses" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Interruptions" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Ratio de pause" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Jour" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Semaine" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mois" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Rien à voir encore" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Terminez quelques Pomodoros pour remplir ceci !" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u jour sauté" msgstr[1] "%u jours sautés" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u semaine sautée" msgstr[1] "%u semaines sautées" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u mois sauté" msgstr[1] "%u mois sautés" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Aujourd'hui" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Hier" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Cette semaine" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "Semaine %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Semaine %u sur %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Courte pause" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Longue pause" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pause" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Ouvrir la superposition d'écran" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "La session a expiré" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Longue pause prévue dans %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Rembobiner d'une minute" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "Vue _compacte" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Préférences" #: src/ui/main/window.ui:19 msgid "_About" msgstr "À propos" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Quitter" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Menu principal" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Laisser le minuteur tourner ?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Vous pouvez le laisser tourner en arrière-plan — les notifications et " "raccourcis clavier fonctionneront toujours." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Exécuter en arrière-plan" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Il est temps de faire une pause" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Fenêtre principale" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Préférer le thème sombre" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Préférer la vue compacte" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Commencer" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "Interrompre" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Modifier l'action personnalisée" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Annuler" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Enregistrer" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nom" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Déclencheur" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Événement" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Exécuter une commande après un événement." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Condition" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Assurer l'exécution d'une seconde commande une fois que la condition n'est " "plus remplie." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Événements" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Ajouter un _événement" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtrer" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filtre" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Commande" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Commandes" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Commande si condition remplie" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Commande si condition non remplie" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Répertoire de travail" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Utiliser un sous-shell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Lancer le programme depuis un sous-shell tel que sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Passer les données d'entrée" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Au lieu de passer des variables, vous pouvez traiter un objet JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Attendre la fin de l'exécution" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Bloquer l'exécution d'autres commandes jusqu'à la fin de la commande." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Supprimer l'action" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Aucun événement spécifié." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Actions personnalisées…" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Ajouter" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Sélectionner le répertoire de travail" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Choisir" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Action sans titre" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Ajouter une condition" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Ajouter un groupe" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 #, fuzzy msgid "AND" msgstr "ET" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 #, fuzzy msgid "OR" msgstr "OU" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Est" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "N'est pas" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Égale" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Plus grand que" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Moins que" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Oui" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Non" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minutes" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Secondes" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Heures" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Choisir un champ…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "État" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "En cours" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Durée" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Insérer une variable" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Journal" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Afficher le journal d'exécution" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Lancer des commandes automatiquement sur des événements ou conditions du " "minuteur. En savoir plus." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Raccourci clavier" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Définir" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Désactiver" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Appuyez sur Échap pour annuler ou Retour arrière pour " "désactiver le raccourci clavier" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Les raccourcis globaux permettent de contrôler l'application même hors " "écran. Ils fonctionnent tant que l'application tourne en arrière-plan." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Ouvrir les paramètres pour modifier les raccourcis globaux" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Modifier" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Saisissez un raccourci pour commencer ou arrêter le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Saisissez un raccourci pour commencer/interrompre/continuer le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Saisissez un nouveau raccourci pour démarrer le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Saisissez un nouveau raccourci pour arrêter le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Saisissez un nouveau raccourci pour interrompre le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Saisissez un nouveau raccourci pour continuer le minuteur" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Saisissez un nouveau raccourci pour sauter" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Rembobiner d'une minute" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Saisissez un nouveau raccourci pour rembobiner" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Saisissez un nouveau raccourci pour mettre la fenêtre au premier plan" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Annonces" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Temps presque écoulé" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "" "Prévenir quand le Pomodoro ou la pause est sur le point de se terminer." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Une notification plein écran destinée à forcer la prise d'une pause." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Délai de verrouillage" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Période d'inactivité avant de verrouiller l'écran." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Délai de réouverture" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Période d'inactivité avant de rouvrir la superposition après fermeture." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Jamais" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notifications" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Sons" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Apparence" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Raccourcis clavier" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatisation" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Les sons sont désactivés" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Sons d'alerte" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Son de fin de Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Son de fin de pause" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Son d'ambiance" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Cloche" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Cloche forte" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Horloge" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Aucun" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volume :" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Sélectionnez un son personnalisé" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Durée d'un Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Durée d'une courte pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Durée d'une longue pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Nombre de cycles" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Comportement" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Interrompre en verrouillant l'écran" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Confirmer le début d'une pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Confirmer le début d'un Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Une session unique prendra %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% du temps sera alloué aux pauses." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Appliquer les changements au Pomodoro en cours ?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Appliquer les changements à la pause en cours ?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Appliquer" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Barre latérale" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Utilitaire de gestion du temps" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Restez concentré en prenant des pauses fréquentes" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Notifications visuelles et sonores" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Suivi du temps et statistiques" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Intégration au bureau GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "" #~ "Exécuter des commandes personnalisées après un Pomodoro ou une pause" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Minuteur compact" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Ajout de la traduction tamoule (merci @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Ajout de la traduction hébraïque (merci @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Support de GNOME Shell 49 (merci @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Mise à jour de la traduction allemande (merci @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Support de GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Répartir le temps passé après minuit" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Ajout de la traduction télougou (merci @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Support de GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Permettre d'ignorer la superposition par geste pendant une vidéo" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Ajout de la traduction géorgienne (merci @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Ajustement des traductions dans appdata (merci @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "" #~ "Correction du maintien de la notification après extension du Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Corrections pour GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Fin du support pour GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Aperçu des changements dans gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Support de GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Ajustement du script de construction pour meson 0.59.0 (merci @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Laissez Pomodoro gérer les notifications système pendant que le " #~ "minuteur tourne" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 secondes" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 secondes" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minute" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minutes" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minutes" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minutes" #~ msgid "Timer Ticking" #~ msgstr "Minuteur" #, fuzzy #~ msgid "Birds" #~ msgstr "Oiseaux" #~ msgid "timer;" #~ msgstr "minuteur;pomodoro;" #, fuzzy #~ msgid "Start/Stop" #~ msgstr "Commencer/Arrêter" #, fuzzy #~ msgid "Pause/Resume" #~ msgstr "Interrompre/Continuer" #, fuzzy #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Passer au prochain pomodoro ou à la pause" #~ msgid "Reset current session" #~ msgstr "Réinitialiser la session en cours" #~ msgid "Run as background service" #~ msgstr "Exécuter en tant que service d'arrière-plan" #~ msgid "About Pomodoro" #~ msgstr "À propos de Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Un outil simple de gestion du temps" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Arrêter" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indicateur pour GNOME Shell" #, fuzzy #~ msgid "_Install" #~ msgstr "Installer" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Durée d'une pause longue" #~ msgid "A time management utility for GNOME" #~ msgstr "Un utilitaire de gestion du temps pour GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Un utilitaire GNOME qui aide à la gestion du temps selon la technique " #~ "Pomodoro. Il vise à améliorer la productivité et la concentration en " #~ "aménageant de courtes pauses toutes les 25 minutes de travail." #~ msgid "Timer window" #~ msgstr "Fenêtre de la minuterie" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indicateur pour GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indicateur pour GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indicateur pour GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indicateur pour GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Minuteur" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Raccourci clavier pour basculer la minuterie. Entrez le nouveau raccourci " #~ "à modifier." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoros avant une longue pause" #~ msgid "Keyboard shortcut" #~ msgstr "Raccourci clavier" #~ msgid "Screen notifications" #~ msgstr "Notification à l'écran" #~ msgid "Wait for activity after a break" #~ msgstr "Attendre qu'il y ait de l'activité après une pause" #~ msgid "Plugins…" #~ msgstr "Greffons…" #~ msgid "Plugins" #~ msgstr "Greffons" #~ msgid "Back" #~ msgstr "Retour" #~ msgid "Complete a few sessions" #~ msgstr "Effectuez quelques séances" #~ msgid "Previous (Alt+Left)" #~ msgstr "Précédent (Alt+Gauche)" #~ msgid "Next (Alt+Right)" #~ msgstr "Suivant (Alt+Droite)" #~ msgid "Complete" #~ msgstr "Terminé" #~ msgid "Enable" #~ msgstr "Activer" #~ msgid "Add" #~ msgstr "Ajouter" #~ msgid "Remove" #~ msgstr "Supprimer" #~ msgid "Elapsed Time" #~ msgstr "Temps écoulé" #~ msgid "Pause Timer" #~ msgstr "Interrompre" #~ msgid "Pause break" #~ msgstr "Interrompre pause" #~ msgid "Pause Pomodoro" #~ msgstr "Interrompre pomodoro" #~ msgid "Resume break" #~ msgstr "Continuer la pause" #~ msgid "Resume Pomodoro" #~ msgstr "Continuer pomodoro" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minute restante" #~ msgstr[1] "%d minutes restantes" #~ msgid "Report issue" #~ msgstr "Signaler un problème" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Impossible d'exécuter le service %s" #~ msgid "Woodland Birds" #~ msgstr "Oiseaux forestiers" #~ msgid "End of Break Sound" #~ msgstr "Son de fin de pause" #~ msgid "Start of Break Sound" #~ msgstr "Son de début de pause" #~ msgid "Off" #~ msgstr "Arrêt" #~ msgid "Ticking sound" #~ msgstr "Tic-tac" #~ msgid "Start of break sound" #~ msgstr "Son de début de pause" #~ msgid "End of break sound" #~ msgstr "Son de fin de pause" #~ msgid "Focus on your task." #~ msgstr "Concentrez-vous sur votre tâche." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Vous avez %d minutes" #~ msgstr[1] "%d minutes" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Vous avez %d seconde avant le prochain pomodoro" #~ msgstr[1] "Vous avez %d secondes avant le prochain pomodoro" #~ msgid "Take a longer break" #~ msgstr "Faites une pause plus longue" #~ msgid "Lengthen it" #~ msgstr "Prolonger la pause" #~ msgid "Shorten it" #~ msgstr "Raccourcir la pause" #~ msgid "Start pomodoro" #~ msgstr "Démarrer un pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "L'utilisation de \"%s\" comme raccourci interférera avec la saisie. " #~ "Essayez d'ajouter une autre touche, telle que Contrôle, Alt ou Maj." #~ msgid "Available" #~ msgstr "Disponible" #~ msgid "Busy" #~ msgstr "Occupé" #~ msgid "Idle" #~ msgstr "Au repos" #~ msgid "Invisible" #~ msgstr "Invisible" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "time;timer;tasks;manage;organize;" #~ msgstr "temps;minuteur;tâches;gérer;organiser;" #, javascript-format #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d nouveau message" #~ msgstr[1] "%d nouveaux messages" #, javascript-format #~ msgid "%ds" #~ msgstr "%ds" #~ msgid "Take a break!" #~ msgstr "Faites une pause !" #, javascript-format #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Vous avez %d minute avant le prochain pomodoro." #~ msgstr[1] "Vous avez %d minutes avant le prochain pomodoro." #~ msgid "Hey!" #~ msgstr "Hé !" #~ msgid "You're missing out on a break" #~ msgstr "Vous êtes en train de rater une pause" #~ msgid "during pomodoro" #~ msgstr "pendant un pomodoro" #~ msgid "during break" #~ msgstr "pendant une pause" #~ msgid "Indicator for Pomodoro will show up after you restart your desktop." #~ msgstr "L'indicateur de Pomodoro apparaîtra au redémarrage de votre bureau." #~ msgid "Extension does not support shell version" #~ msgstr "L'extension n'est pas compatible avec la version du shell" #~ msgid "You need to upgrade Pomodoro." #~ msgstr "Vous devez mettre à niveau Pomodoro." #~ msgid "Upgrade" #~ msgstr "Mettre à niveau" #, c-format #~ msgid "Could not find extension \"%s\" in \"%s\"" #~ msgstr "Impossible de trouver l'extension \"%s\" dans \"%s\"" #~ msgid "Error loading extension" #~ msgstr "Erreur lors du chargement de l'extension" #~ msgid "Pomodoro extension is disabled" #~ msgstr "L'extension Pomodoro est désactivée" #~ msgid "Extension provides better desktop integration for the pomodoro app." #~ msgstr "" #~ "L'extension procure une meilleure intégration au bureau pour " #~ "l'application pomodoro." #, c-format #~ msgid "" #~ "The shortcut \"%s\" cannot be used because it will become impossible to " #~ "type using this key.\n" #~ "Please try with a key such as Control, Alt or Shift at the same time." #~ msgstr "" #~ "Le raccourci \"%s\" ne peut pas être utilisé parce qu'il deviendrait " #~ "impossible de taper en utilisant cette touche.\n" #~ "Merci d'essayer avec une touche telle que Ctrl, Alt ou Maj en même temps." #~ msgid "On" #~ msgstr "Marche" #~ msgid "Change Presence Status" #~ msgstr "Changer le statut de présence" #~ msgid "Status during pomodoro" #~ msgstr "Statut pendant un pomodoro" #~ msgid "Empathy" #~ msgstr "Empathy" #~ msgid "Set custom status" #~ msgstr "Définir un statut personnalisé" #~ msgid "Remind to take a break" #~ msgstr "Rappeler de faire une pause" #~ msgid "Wake up screen" #~ msgstr "Écran de réveil" #~ msgid "Select sound for end of break" #~ msgstr "Choisir un son de fin de pause" #~ msgid "Short Text" #~ msgstr "Texte court" #~ msgid "Presence" #~ msgstr "Présence" #~ msgid "Change presence status" #~ msgstr "Changer le statut de présence" #~ msgid "_No sound" #~ msgstr "Pas de so_n" #~ msgid "_Open" #~ msgstr "_Ouvrir" #~ msgid "All files" #~ msgstr "Tous les fichiers" #~ msgid "Supported audio files" #~ msgstr "Fichiers audio pris en charge" #~ msgid "Looks like gnome-pomodoro is not installed" #~ msgstr "Il semble que gnome-pomodoro ne soit pas installé" #~ msgid "A new pomodoro is starting" #~ msgstr "Un nouveau pomodoro démarre" #~ msgid "Problem with pomodoro" #~ msgstr "Problème avec pomodoro" #~ msgid "" #~ "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; either version 3 of the License, or (at your " #~ "option) any later version." #~ msgstr "" #~ "Ce programme est un logiciel libre : vous pouvez le redistribuer ou le " #~ "modifier suivant les termes de la GNU General Public License telle que " #~ "publiée par la Free Software Foundation ; soit la version 3 de la " #~ "licence, soit (à votre gré) toute version ultérieure." #~ msgid "Select sound for pomodoro start" #~ msgstr "Choisir le son pour le démarrage d’un pomodoro" #~ msgid "Postpone pomodoro when idle" #~ msgstr "Reporter le pomodoro quand inactif" #~ msgid "" #~ "System notifications including chat messages won't show up during " #~ "pomodoro." #~ msgstr "" #~ "Les notifications systèmes y compris les messages instantanés " #~ "n'apparaîtront pas pendant les pomodoros." #~ msgid "" #~ "System notifications including chat messages won't show up during break." #~ msgstr "" #~ "Les notifications systèmes y compris les messages instantanés " #~ "n'apparaîtront pas pendant les pauses." #~ msgid "System notifications including chat messages won't show up." #~ msgstr "" #~ "Les notifications systèmes y compris les messages instantanés " #~ "n'apparaîtront pas." #~ msgid "OK" #~ msgstr "OK" #~ msgid "Manage your time and tasks" #~ msgstr "Gérez votre temps et vos tâches" #~ msgid "Could not run pomodoro" #~ msgstr "Impossible de lancer le pomodoro" #, fuzzy #~ msgid "Reset Counts and Timer" #~ msgstr "Réinitialiser le compteur et le minuteur" #, fuzzy #~ msgid "Away From Desk" #~ msgstr "Éloigné du poste de travail" #, fuzzy #~ msgid "%d Completed Session" #~ msgid_plural "%d Completed Sessions" #~ msgstr[0] "%d pomodoro terminé" #~ msgstr[1] "%d pomodoros terminés" #, fuzzy #~ msgid "Time in seconds you are supposed to be working." #~ msgstr "Temps en secondes que vous êtes censé travailler." #, fuzzy #~ msgid "Time in seconds you are supposed to have a short break." #~ msgstr "Temps en secondes que vous êtes censé faire en pause courte." #~ msgid "Long pause duration" #~ msgstr "Durée de la pause longue" #, fuzzy #~ msgid "Time in seconds you are supposed to have a longer break." #~ msgstr "Temps en secondes que vous êtes censé faire en pause longue." #, fuzzy #~ msgid "Whether to show a notification dialog when pause starts." #~ msgstr "Afficher une notification lors du démarrage d’une pause." #~ msgid "Disable flexible breaks" #~ msgstr "Désactiver les pauses flexibles" #, fuzzy #~ msgid "Whether you are not using a computer to work." #~ msgstr "Si vous n’utilisez pas un ordinateur pour travailler." #, fuzzy #~ msgid "Whether to change user and IM presence to busy." #~ msgstr "Changer le statut de présence de l’utilisateur en Occupé." #, fuzzy #~ msgid "Whether to play a sound to notify of events." #~ msgstr "S'il faut jouer un son pour indiquer les notifications." #~ msgid "Notification sound file" #~ msgstr "Fichier du son de notification" #, fuzzy #~ msgid "Restore timer state" #~ msgstr "Restaurer l’état du minuteur" #, fuzzy #~ msgid "Whether to restore state on startup." #~ msgstr "S'il faut restaurer l’état du minuteur au démarrage." #~ msgid "Number of completed sessions since long break" #~ msgstr "Nombre de pomodoro terminés depuis la longue pause" #, fuzzy #~ msgid "Saved timer state" #~ msgstr "État du minuteur sauvegardé" #, fuzzy #~ msgid "Time of saved state" #~ msgstr "Date de la sauvegarde du minuteur" #, fuzzy #~ msgid "Click to reset session counts to zero" #~ msgstr "Cliquer pour remettre à zéro le compteur de pomodoros" #, fuzzy #~ msgid "Set optimal settings for doing paperwork" #~ msgstr "Utiliser les meilleurs paramètres pour s’occuper de la paperasse" #, fuzzy #~ msgid "Show Dialog Messages" #~ msgstr "Afficher la boîte de dialogue" #, fuzzy #~ msgid "Show a dialog message at the end of pomodoro session" #~ msgstr "Afficher une boîte de dialogue à la fin d’un pomodoro" #~ msgid "Play a sound at start of pomodoro session" #~ msgstr "Jouer un son au début d’un pomodoro" #~ msgid "Hide" #~ msgstr "Masquer" #~ msgid "Timer toggle key" #~ msgstr "Raccourci d’activation du minuteur" focustimerhq-FocusTimer-8581be2/po/he.po000066400000000000000000001741061520625676500202420ustar00rootroot00000000000000# Hebrew translation for focus-timer # Copyright (c) 2025 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Omer I.S. , 2025. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2025-12-11 22:52+0200\n" "Last-Translator: Omer I.S. \n" "Language-Team: Hebrew\n" "Language: he\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=4; plural=(n==1 ? 0 : n==2 ? 1 : n>10 && n%10==0 ? " "2 : 3);\n" "X-Generator: Poedit 3.8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "קוצב זמן ליעילות המסייע לך לעבוד בצורה אפקטיבית יותר על ידי חלוקת הזמן שלך " "לסבבי עבודה ממוקדים שאחריהם הפסקות קצרות. עבוד במשך 25 דקות, ולאחר מכן צא " "להפסקה של 5 דקות כדי לשמור על ריכוז ולמנוע שחיקה." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "תכונות עיקריות:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "אפשרות להתאמה אישית של משך סבבי העבודה וההפסקות" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "שכבת כיסוי למסך בזמן הפסקות" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "קוצב זמן" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "סטטיסטיקה יומית" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "סטטיסטיקה חודשית" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "העדפות" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "כיסוי מסך" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "התחלה או עצירה" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "התחלה, השהיה או המשך" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "התחלת פומודורו" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "התחלה" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "עצירה" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "השהיה" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "המשך" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "דילוג" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "חזרה לאחור" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "הארכת הפומודורו או ההפסקה הנוכחיים" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "איפוס" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "הצגת ההעדפות" #: src/application.vala:203 msgid "Quit application" msgstr "יציאה מהיישום" #: src/application.vala:206 msgid "Print version information and exit" msgstr "הצגת המידע על הגרסה ויציאה" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "הבאה למיקוד" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "נותרו %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "הפעולה המותאמת אישית \"%s\" נכשלה" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "הזמן הקצוב הסתיים" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "נכשל ביצוע הפקודה" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "הפקודה ריקה" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "מירכאות לא סגורות" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "פקודה לא חוקית" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "משתנה לא ידוע \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "פורמט לא ידוע \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "התוכנית \"%s\" לא נמצאה" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "פעולות" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "ספירה לאחור" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "סשן" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "אחר" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "הפעלת קוצב הזמן." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "עצירת קוצב הזמן באופן ידני." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "הספירה לאחור הושהתה ידנית. לא מופעל בעת נעילת המסך או השהיית המערכת." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "הספירה לאחור חודשה באופן ידני." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "מעבר לבלוק הזמן הבא לפני סיום הספירה לאחור." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "נעשה שימוש בפעולת חזרה לאחור. פעולה זו מוסיפה השהיה בדיעבד." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "ניקוי הסשן באופן ידני." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "הסתיים" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "הספירה לאחור הסתיימה. אם ממתינים לאישור, ייתכן שמשך בלוק הזמן עדיין ישתנה." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "השתנה" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "מופעל בכל שינוי הקשור לספירה לאחור." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "אישור התקדמות" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "נדרש אישור ידני כדי להתחיל את בלוק הזמן הבא." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "התקדם" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "מעבר או דילוג לבלוק הזמן הבא." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "שינוי מצב" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "מעבר לבלוק הזמן הבא או כאשר הפסקה מוגדרת מחדש." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "תוזמן מחדש" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "מופעל כאשר בלוקי זמן מתוזמנים השתנו." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "פג תוקף" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "מופעל כאשר הסשן עומד להתאפס עקב חוסר פעילות." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "פומודורו" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "לקיחת הפסקה" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "הגיע הזמן להפסקה קצרה" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "לקיחת הפסקה ארוכה" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "הפומודורו עומד להסתיים" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "צאו להפסקה" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "ההפסקה עומדת להסתיים" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 דקה" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "זמן להתכונן…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "הפומודורו נגמר!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "ההפסקה נגמרה!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "אישור התחלת פומודורו…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "אישור התחלת הפסקה…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "אישור התחלת הפסקה קצרה…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "אישור התחלת הפסקה ארוכה…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "דילוג על ההפסקה" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "נכשלהחול הנגינה" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "קובץ לא נמצא" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "סוג קובץ לא נתמך" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "נעצר" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "הפסקה" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "הפסקה קצרה" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "הפסקה ארוכה" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u שע׳" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u דק׳" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "שעה %u" msgstr[1] "שעתיים" msgstr[2] "%u שעות" msgstr[3] "%u שעות" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "דקה אחת" msgstr[1] "%u דקות" msgstr[2] "%u דקות" msgstr[3] "%u דקות" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "שנייה אחת" msgstr[1] "%u שניות" msgstr[2] "%u שניות" msgstr[3] "%u שניות" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "הזמן המדויק של האירוע הנוכחי." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "השלב הנוכחי במחזור הפומודורו. ערכים אפשריים: stopped (עצור), " "pomodoro (פומודורו), break (הפסקה), short-break " "(הפסקה קצרה), long-break (הפסקה ארוכה)." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "סטטוס בלוק הזמן הנוכחי. ערכים אפשריים: scheduled (מתוזמן), in-" "progress (בביצוע), completed (הושלם), uncompleted (לא " "הושלם)." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "דגל המציין אם הספירה לאחור החלה." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "דגל המציין אם הספירה לאחור מושהית." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "דגל המציין אם הספירה לאחור הסתיימה." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "דגל המציין אם קוצב הזמן פועל באופן פעיל." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "משך הספירה לאחור הנוכחית." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "ההפרש בין הזמן שחלף לבין הזמן שעבר בפועל." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "משך הזמן שהושקע בספירה לאחור." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "משך הזמן שנותר עד לסיום הספירה לאחור." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "הזמן בו החלה הספירה לאחור." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "הרחבה ל־GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "קבלו את החוויה הטובה ביותר!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "הפעילו את הרחבת GNOME Shell לשילוב חלק בשולחן העבודה" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "תמיד בהישג יד" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "שלטו בקוצב הזמן ישירות מהסרגל העליון ללא פתיחת האפליקציה" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "פחות הסחות דעת" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "תזכורות הפסקה משופרות" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "כיסוי מסך מלא ואלגנטי שהופך את היציאה להפסקה לחוויה נעימה יותר" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "מוכנים לנסות?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "התקנת _הרחבה" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_לא עכשיו" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "משהו השתבש" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "העתקה ללוח" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_ניסיון חוזר" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_ביטול" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "הזמן הקצוב הסתיים" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "התקנת הרחבות אינה מורשית" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "הורדת ההרחבה נכשלה" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "כיסוי מסך" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "שולחן העבודה" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "הרחבה ל־GNOME Shell זמינה" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "מידע נוסף" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "לא בשימוש" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "הסתיים!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "סטטיסטיקה" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "יציאה" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "יומן" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "יומן ריק" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "רישומים יופיעו כאן ברגע שתפעילו את קוצב הזמן." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "הקשר" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "פקודה" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "פלט" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "שגיאה" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "קוד יציאה:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "זמן ביצוע:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "עומר א״ש " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "תרומה" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "הפסקות" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "הפרעות" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "יחס הפסקות" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "יום" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "שבוע" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "חודש" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "עדיין אין מה לראות כאן" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "השלימו כמה סבבי פומודורו כדי למלא את הגרף!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "יום אחד דולג" msgstr[1] "יומיים דולגו" msgstr[2] "%u ימים דולגו" msgstr[3] "%u ימים דולגו" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "שבוע אחד דולג" msgstr[1] "שבועיים דולגו" msgstr[2] "%u שבועות דולגו" msgstr[3] "%u שבועות דולגו" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "חודש אחד דולג" msgstr[1] "חודשיים דולגו" msgstr[2] "%u חודשים דולגו" msgstr[3] "%u חודשים דולגו" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "היום" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "אתמול" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "השבוע" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "שבוע %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "שבוע %u מתוך %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_פומודורו" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "הפסקה _קצרה" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "הפסקה ארו_כה" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_הפסקה" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "פתיחת כיסוי המסך" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "הסשן פג" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "הפסקה ארוכה בעוד %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "חזרה דקה אחת אחורה" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "תצוגה _קומפקטית" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "ה_עדפות" #: src/ui/main/window.ui:19 msgid "_About" msgstr "על _אודות" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "י_ציאה" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "תפריט ראשי" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "להשאיר את קוצב הזמן פועל?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "ניתן להמשיך להריץ אותו ברקע — התרעות וקיצורי מקשים ימשיכו לפעול." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "הפעלה ברקע" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "הגיע הזמן לקחת הפסקה" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "חלון ראשי" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "שימוש בערכת נושא כהה" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "העדפת תצוגה קומפקטית" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "התחיל" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "מושהה" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "עריכת פעולה מותאמת אישית" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "בי_טול" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_שמירה" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "שם" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "טריגר" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "אירוע" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "ביצוע פקודה לאחר אירוע." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "תנאי" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "הבטחת ביצוע פקודה שנייה ברגע שהתנאי כבר אינו מתקיים." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "אירועים" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "הוספת _אירוע" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_סינון" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "מסנן" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "פקודת Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "פקודות" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "פקודה להתקיים התנאי" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "פקודה לאי־קיום התנאי" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "ספריית עבודה" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "שימוש ב־Subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "הפעלת התוכנית מתוך subshell כגון sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "העברת נתוני קלט" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "במקום להעביר משתנים, ניתן לעבד אובייקט JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "המתנה לסיום" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "חסימת ביצוע פקודות אחרות עד לסיום הפקודה." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_מחיקת פעולה" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "עדיין לא צוינו אירועים." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "הוספת פעולה מותאמת אישית" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_הוספה" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "בחירת ספריית עבודה" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "ב_חירה" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "פעולה ללא שם" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "הוספת תנאי" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "הוספת קבוצה" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "וגם" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "או" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "הוא" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "אינו" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "שווה ל־" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "גדול מ־" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "קטן מ־" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "כן" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "לא" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "דקות" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "שניות" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "שעות" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "בחירת שדה…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "מצב" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "פועל" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "משך זמן" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "הכנסת משתנה" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "פורמט" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_יומן" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "הצגת יומן ביצוע" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "הפעלת פקודות shell באופן אוטומטי באירועי קוצב זמן או תנאים מסוימים. מידע נוסף." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "הגדרת קיצור מקשים" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_הגדרה" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "מושבת" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "לחצו על Esc לביטול או על Backspace להשבתת קיצור המקשים" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "קיצורי מקשים גלובליים מאפשרים לשלוט באפליקציה גם כשהיא לא על המסך. הם פועלים " "כל עוד האפליקציה רצה ברקע." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "פתיחת הגדרות האפליקציה לעריכת קיצורי מקשים גלובליים" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_עריכה" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "הקישו קיצור מקשים חדש להתחלה או עצירה של קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "הקישו קיצור מקשים חדש להתחלה/השהיה/המשך של קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "הקישו קיצור מקשים חדש להתחלת קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "הקישו קיצור מקשים חדש לעצירת קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "הקישו קיצור מקשים חדש להשהיית קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "הקישו קיצור מקשים חדש להמשך קוצב הזמן" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "הקישו קיצור מקשים חדש לדילוג" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "חזרה דקה אחת אחורה" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "הקישו קיצור מקשים חדש לחזרה לאחור" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "הקישו קיצור מקשים חדש להבאת החלון למיקוד" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "הודעות" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "הזמן אוזל" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "קבלת התרעה כאשר הפומודורו או ההפסקה עומדים להסתיים." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "התרעה על מסך מלא שנועדה לאכוף יציאה להפסקה." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "השהיית נעילה" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "משך זמן חוסר הפעילות שלאחריו יינעל המסך." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "השהיית פתיחה מחדש" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "משך זמן חוסר הפעילות שלאחריו ייפתח הכיסוי שוב לאחר שנסגר." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "אף פעם" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "התרעות" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "צלילים" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "מראה" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "קיצורי מקשים" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "אוטומציה" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "הצלילים מושבתים" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "צלילי התרעה" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "צליל סיום פומודורו" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "צליל סיום הפסקה" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "צליל רקע" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "פעמון" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "פעמון חזק" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 #, fuzzy msgid "Clock Ticking" msgstr "תקתוק שעון" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "ללא" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "עוצמת שמע:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "בחירת צליל מותאם אישית" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "משך פומודורו" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "משך הפסקה קצרה" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "משך הפסקה ארוכה" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "מספר מחזורים" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "התנהגות" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "השהיה בעת נעילת המסך" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "אישור התחלת הפסקה" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "אישור התחלת פומודורו" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "סשן בודד יימשך %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% מהזמן יוקצה להפסקות." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "להחיל שינויים על הפומודורו הנוכחי?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "להחיל שינויים על ההפסקה הנוכחית?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "החלה" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "" #, fuzzy #~ msgid "Time management utility" #~ msgstr "כלי עזר לניהול זמן" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "שמירה על ריכוז באמצעות הפסקות תכופות" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "התרעות חזותיות וקוליות" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "מעקב זמן וסטטיסטיקה" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "שילוב בשולחן העבודה של GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "הפעלת פקודות מותאמות אישית לאחר פומודורו או הפסקה" #, fuzzy #~ msgid "Compact timer" #~ msgstr "קוצב זמן קומפקטי" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "נוסף תרגום לטמילית (תודה ל־@omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "נוסף תרגום לעברית (תודה ל־@Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "תמיכה ב־GNOME Shell 49 (תודה ל־@aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "עודכן התרגום לגרמנית (תודה ל־@daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "תמיכה ב־GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "פיצול זמן העבודה במעבר בין ימים (חצות)" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "נוסף תרגום לטלוגו (תודה ל־@SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "תמיכה ב־GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "אפשרות לסגור את כיסוי המסך במחווה בזמן שמתנגן וידאו" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "נוסף תרגום לגיאורגית (תודה ל־@NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "התאמת תרגומים ב־appdata (תודה ל־@yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "תיקון הישארות התרעה לאחר הארכת הפומודורו" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "תיקונים עבור GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "הפסקת התמיכה ב־GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "סקירת שינויים ב־gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "תמיכה ב־GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "התאמת סקריפט הבנייה ל־meson 0.59.0 (תודה ל־@mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "אפשרו לפומודורו לנהל את התרעות המערכת בזמן שקוצב הזמן פועל" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 שניות" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 שניות" #, fuzzy #~ msgid "1 minute" #~ msgstr "דקה אחת" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 דקות" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 דקות" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 דקות" #, fuzzy #~ msgid "Timer Ticking" #~ msgstr "תקתוק קוצב זמן" #, fuzzy #~ msgid "Birds" #~ msgstr "ציפורים" #~ msgid "timer;" #~ msgstr "timer;טיימר;קוצב;זמן;" #~ msgid "Start/Stop" #~ msgstr "התחלה/עצירה" #~ msgid "Pause/Resume" #~ msgstr "השהיה/המשך" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "דילוג לפומודורו או להפסקה" #~ msgid "Reset current session" #~ msgstr "איפוס הסשן הנוכחי" #~ msgid "Run as background service" #~ msgstr "הפעלה בתור שירות רקע" #~ msgid "About Pomodoro" #~ msgstr "על אודות פומודורו" #~ msgid "A simple time management utility" #~ msgstr "כלי עזר פשוט לניהול זמן" #, fuzzy #~ msgid "_Stopped" #~ msgstr "עצור" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "הרחבה ל־GNOME Shell זמינה" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "הפסקה ארוכה בעוד %s" #~ msgid "A time management utility for GNOME" #~ msgstr "כלי עזר לניהול זמן עבור GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "כלי עזר ל־GNOME שמסייע בניהול הזמן לפי שיטת פומודורו. הוא נועד לשפר את " #~ "התפוקה והמיקוד בעזרת לקיחת הפסקות קצרות לאחר כל 25 דקות של עבודה." #~ msgid "Timer window" #~ msgstr "חלון קוצב הזמן" #~ msgid "Indicator for GNOME Shell" #~ msgstr "מחוון למעטפת GNOME" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "תמיכה ב־GNOME Shell 4.0" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "תמיכה ב־GNOME Shell 3.36" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "תמיכה ב־GNOME Shell 3.34 בלבד" #~ msgid "_Timer" #~ msgstr "קוצב _זמן" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "קיצור מקשים להתחלה או עצירה של קוצב הזמן. לחיצה על צירוף מקשים חדש תשנה " #~ "אותו." #~ msgid "Pomodoros before a long break" #~ msgstr "מספר סבבי פומודורו לפני הפסקה ארוכה" #~ msgid "Keyboard shortcut" #~ msgstr "צירוף מקשים" #~ msgid "Screen notifications" #~ msgstr "התרעות על גבי המסך" #~ msgid "Wait for activity after a break" #~ msgstr "המתנה לפעילות לאחר הפסקה" #~ msgid "Plugins…" #~ msgstr "תוספים…" #~ msgid "Plugins" #~ msgstr "תוספים" #~ msgid "Back" #~ msgstr "חזרה" #~ msgid "Previous (Alt+Left)" #~ msgstr "הקודם (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "הבא (Alt+Right)" #~ msgid "Complete" #~ msgstr "הושלם" #~ msgid "Enable" #~ msgstr "הפעלה" #~ msgid "Add" #~ msgstr "הוספה" #~ msgid "Remove" #~ msgstr "הסרה" #~ msgid "Elapsed Time" #~ msgstr "הזמן שחלף" #~ msgid "Pause Timer" #~ msgstr "השהיית קוצב הזמן" #~ msgid "Pause break" #~ msgstr "השהיית הפסקה" #~ msgid "Pause Pomodoro" #~ msgstr "השהיית פומודורו" #~ msgid "Resume break" #~ msgstr "המשך הפסקה" #~ msgid "Resume Pomodoro" #~ msgstr "המשך פומודורו" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "נותרה דקה אחת (%d)" #~ msgstr[1] "נותרו %d דקות" #~ msgstr[2] "נותרו %d דקות" #~ msgstr[3] "נותרו %d דקות" #~ msgid "Report issue" #~ msgstr "דיווח על בעיה" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "נכשלה הרצת שירות %s" #~ msgid "End of Break Sound" #~ msgstr "צליל סיום הפסקה" #~ msgid "Start of Break Sound" #~ msgstr "צליל התחלת הפסקה" #~ msgid "Off" #~ msgstr "כבוי" #~ msgid "Start of break sound" #~ msgstr "צליל התחלת הפסקה" #~ msgid "End of break sound" #~ msgstr "צליל סיום הפסקה" #~ msgid "Focus on your task." #~ msgstr "יש להתמקד במשימה שלך." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "נותרה לך שנייה אחת (%d)" #~ msgstr[1] "נותרו לך %d דקות" #~ msgstr[2] "נותרו לך %d דקות" #~ msgstr[3] "נותרו לך %d דקות" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "נותרה לך שנייה אחת (%d)" #~ msgstr[1] "נותרו לך %d שניות" #~ msgstr[2] "נותרו לך %d שניות" #~ msgstr[3] "נותרו לך %d שניות" #~ msgid "Take a longer break" #~ msgstr "הגיע הזמן להפסקה ארוכה" #~ msgid "Lengthen it" #~ msgstr "להאריך" #~ msgid "Shorten it" #~ msgstr "לקצר" #~ msgid "Start pomodoro" #~ msgstr "התחלת פומודורו" #~ msgid "Available" #~ msgstr "זמין" #~ msgid "Busy" #~ msgstr "עסוק" #~ msgid "Idle" #~ msgstr "בהמתנה" #~ msgid "Invisible" #~ msgstr "בלתי נראה" #, c-format #~ msgid "%d m" #~ msgstr "%d דק׳" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f שע׳" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f שע׳" focustimerhq-FocusTimer-8581be2/po/hr.po000066400000000000000000002014251520625676500202520ustar00rootroot00000000000000# Croatian translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Milo Ivir , 2021. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 11:45+0100\n" "Last-Translator: Milo Ivir \n" "Language-Team: Croatian\n" "Language: hr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" "X-Generator: Poedit 3.1.1\n" "X-Poedit-SourceCharset: UTF-8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Timer za produktivnost koji vam pomaže raditi učinkovitije dijeleći vaše " "vrijeme na usredotočene sesije rada praćene kratkim pauzama. Radite 25 " "minuta, a zatim uzmite 5 minuta pauze kako biste zadržali koncentraciju i " "spriječili sagorijevanje." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Glavne značajke:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Prilagodljivo trajanje sesija rada i pauza" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Prekrivanje zaslona tijekom pauza" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Timer" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Dnevna statistika" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Mjesečna statistika" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Postavke" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Prekrivanje zaslona" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 #, fuzzy msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "Aktualiziran prijevod za ruski (@rkaverin)" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Pokreni ili prekini" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Pokreni, zaustavi ili nastavi" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Pokreni Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Pokreni" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Prekini" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Zaustavi" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Nastavi" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Preskoči" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Vrati unatrag" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Produžite trenutni pomodoro ili pauzu" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Ponovno postavi" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Prikaži postavke" #: src/application.vala:203 msgid "Quit application" msgstr "Zatvori program" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Ispiši informacije o verziji i izađi" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Stavi u fokus" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "preostalo %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Prilagođena radnja \"%s\" nije uspjela" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Vrijeme je isteklo" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Neuspjelo izvršavanje naredbe" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Naredba je prazna" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Nezatvoreni navodnik" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Nevaljana naredba" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Nepoznata varijabla \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Nepoznat format \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Program \"%s\" nije pronađen" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Radnje" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Odbrojavanje" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sesija" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Ostalo" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Timer je pokrenut." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Timer je ručno prekinut." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Odbrojavanje je ručno zaustavljeno. Ne aktivira se prilikom zaključavanja " "zaslona ili obustave sustava." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Odbrojavanje je ručno nastavljeno." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Prebačeno na sljedeći vremenski blok prije završetka odbrojavanja." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Upotrijebljena je radnja vraćanja unatrag. Dodaje pauzu u prošlosti." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Sesija je ručno očišćena." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Završeno" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Odbrojavanje je završilo. Ako se čeka potvrda, trajanje vremenskog bloka još " "uvijek se može promijeniti." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Promijenjeno" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Aktivira se kod bilo koje promjene vezane uz odbrojavanje." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Potvrda napredovanja" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Potrebna je ručna potvrda za početak sljedećeg vremenskog bloka." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Napredovalo" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Prebačeno ili preskočeno na sljedeći vremenski blok." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Stanje promijenjeno" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Prelazak na sljedeći vremenski blok ili kod promjene naziva pauze." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Ponovno zakazano" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Aktivira se kada se zakazani vremenski blokovi promijene." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Isteklo" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Aktivira se kada se sesija treba ponovno postaviti zbog neaktivnosti." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Uzmite pauzu" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Napravite kratku pauzu" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Napravite dugu pauzu" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro će uskoro završiti" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Uzmite pauzu" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Pauza će uskoro završiti" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuta" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Pripremi se…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Pomodoro je gotov!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Pauza je gotova!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Potvrdite početak Pomodora…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Potvrdite početak pauze…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Potvrdite početak kratke pauze…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Potvrdite početak duge pauze…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Preskoči pauzu" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Neuspjelo pokretanje reprodukcije" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Datoteka nije pronađena" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Vrsta datoteke nije podržana" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Zaustavljeno" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pauza" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Kratka pauza" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Duga pauza" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u sat" msgstr[1] "%u sata" msgstr[2] "%u sati" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minute" msgstr[2] "%u minuta" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekunde" msgstr[2] "%u sekundi" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Točno vrijeme trenutnog događaja." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Trenutna faza Pomodoro ciklusa. Moguće vrijednosti: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status trenutnog vremenskog bloka. Moguće vrijednosti: scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Zastavica koja pokazuje je li odbrojavanje započelo." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Zastavica koja pokazuje je li odbrojavanje zaustavljeno." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Zastavica koja pokazuje je li odbrojavanje završilo." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Zastavica koja pokazuje odbrojava li timer aktivno." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Trajanje trenutnog odbrojavanja." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Nesklad između proteklog vremena i stvarnog vremena." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Količina vremena provedena u odbrojavanju." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Preostalo vrijeme do kraja odbrojavanja." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Vrijeme kada je odbrojavanje započelo." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell proširenje" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Osigurajte si najbolje iskustvo!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Omogućite proširenje za GNOME Shell za besprijekornu integraciju s " "radnom površinom" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Uvijek nadohvat ruke" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Upravljajte timerom izravno s gornje trake bez otvaranja aplikacije" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Manje ometanja" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Profinjeni podsjetnici za pauzu" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegantno prekrivanje preko cijelog zaslona koje pauze čini ugodnijim " "iskustvom" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Spremni isprobati?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Instaliraj proširenje" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Ne sada" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Nešto je pošlo po zlu" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Kopiraj u međuspremnik" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Pokušaj ponovno" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Prekini" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Vrijeme je isteklo" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Instaliranje proširenja nije dopušteno" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Preuzimanje proširenja nije uspjelo" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Prekrivanje zaslona" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Radna površina" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Dostupno je proširenje za GNOME Shell" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Saznajte više" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Neiskorišteno" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Završeno!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistika" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Zatvori program" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Zapisnik" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Prazan zapisnik" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Unosi će se ovdje pojaviti čim pokrenete timer." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Kontekst" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Naredba" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Izlaz" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Pogreška" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Izlazni kod:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Vrijeme izvršavanja:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Milo Ivir " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Donirajte" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pauze" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Prekidi" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Omjer pauza" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dan" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Tjedan" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mjesec" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Ovdje još nema ničega" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Završite nekoliko Pomodora kako biste popunili ovo!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Preskočen %u dan" msgstr[1] "Preskočena %u dana" msgstr[2] "Preskočeno %u dana" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Preskočen %u tjedan" msgstr[1] "Preskočena %u tjedna" msgstr[2] "Preskočeno %u tjedana" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Preskočen %u mjesec" msgstr[1] "Preskočena %u mjeseca" msgstr[2] "Preskočeno %u mjeseci" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Danas" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Jučer" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Ovaj tjedan" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Tjedan %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Tjedan %u od %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Kratka pauza" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Duga pauza" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pauza" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Otvori prekrivanje zaslona" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Sesija je istekla" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Duga pauza slijedi za %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Vrati jednu minutu unatrag" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Kompaktni prikaz" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Postavke" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_O programu" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Zatvori" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Glavni izbornik" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Ostaviti timer da radi?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Možete ga ostaviti da radi u pozadini — obavijesti i tipkovni prečaci će i " "dalje raditi." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Pokreni u pozadini" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Vrijeme je za pauzu" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Glavni prozor" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Preferiraj tamnu temu" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Preferiraj kompaktni prikaz" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Pokrenuto" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Zaustavljeno" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Uredi prilagođenu radnju" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Odustani" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Spremi" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Ime" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Okidač" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Događaj" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Izvrši naredbu nakon događaja." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Uvjet" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "Osiguraj izvršenje druge naredbe kada uvjet više nije ispunjen." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Događaji" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Dodaj _događaj" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtriraj" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Naredba ljuske" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Naredbe" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Naredba kad je uvjet ispunjen" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Naredba kad uvjet nije ispunjen" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Radni direktorij" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Koristi podljusku" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Pokreni program iz podljuske kao što je sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Proslijedi ulazne podatke" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Umjesto prosljeđivanja varijabli možete obraditi JSON objekt." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Čekaj na dovršetak" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Blokiraj izvršavanje drugih naredbi dok se naredba ne dovrši." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Obriši radnju" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Nijedan događaj još nije naveden." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Dodaj prilagođenu radnju" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Dodaj" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Odaberi radni direktorij" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "Oda_beri" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Radnja bez naslova" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Dodaj uvjet" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Dodaj grupu" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 #, fuzzy msgid "AND" msgstr "I" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 #, fuzzy msgid "OR" msgstr "ILI" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Je" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Nije" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Jednako je" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Veće od" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Manje od" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Da" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Ne" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minute" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Sekunde" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Sati" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Odaberi polje…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Stanje" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Radi" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Trajanje" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Umetni varijablu" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Zapisnik" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Prikaži zapisnik izvršavanja" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Automatski pokreći naredbe ljuske na događaje ili uvjete timera. Saznajte više." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Postavi prečac" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Postavi" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Onemogućeno" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Pritisnite Esc za odustajanje ili Backspace za onemogućavanje " "tipkovnog prečaca" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globalni prečaci omogućuju vam upravljanje aplikacijom čak i kad nije na " "zaslonu. Rade sve dok je aplikacija pokrenuta u pozadini." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Otvorite postavke aplikacije za uređivanje globalnih prečaca" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Uredi" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Unesite novi prečac za pokretanje ili prekidanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Unesite novi prečac za pokretanje/zaustavljanje/nastavljanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Unesite novi prečac za pokretanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Unesite novi prečac za prekidanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Unesite novi prečac za zaustavljanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Unesite novi prečac za nastavljanje timera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Unesite novi prečac za preskakanje" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Vrati jednu minutu unatrag" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Unesite novi prečac za vraćanje unatrag" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Unesite novi prečac za stavljanje prozora u fokus" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Objave" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Vrijeme istječe" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Obavijesti kada Pomodoro ili pauza budu blizu kraja." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Obavijest preko cijelog zaslona namijenjena prisiljavanju na pauzu." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Odgoda zaključavanja" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Razdoblje neaktivnosti nakon kojeg se zaslon zaključava." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Odgoda ponovnog otvaranja" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Razdoblje neaktivnosti nakon kojeg se prekrivanje ponovno otvara nakon što " "se odbaci." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Nikada" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Obavijesti" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Zvukovi" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Izgled" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Tipkovni prečaci" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatizacija" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Zvukovi su onemogućeni" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Zvukovi upozorenja" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Zvuk završetka Pomodora" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Zvuk završetka pauze" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Pozadinski zvuk" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Zvono" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Glasno zvono" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Otkucavanje sata" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Ništa" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Glasnoća:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Odaberi prilagođeni zvuk" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Trajanje Pomodora" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Trajanje kratke pauze" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Trajanje duge pauze" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Broj ciklusa" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Ponašanje" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Zaustavi kod zaključavanja zaslona" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Potvrdi početak pauze" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Potvrdi početak Pomodora" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Pojedinačna sesija će trajati %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% vremena bit će dodijeljeno za pauze." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Primijeniti promjene na trenutni Pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Primijeniti promjene na trenutnu pauzu?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Primijeni" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Bočna traka" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Uslužni program za upravljanje vremenom" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Ostanite usredotočeni uz česte pauze" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Vizualne i zvučne obavijesti" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Praćenje vremena i statistika" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integracija s GNOME radnom površinom" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Pokrenite prilagođene naredbe nakon Pomodora ili pauze" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Kompaktni timer" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Pregled promjena u gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Dodan tamilski prijevod (zahvaljujući @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Dodan hebrejski prijevod (zahvaljujući @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Pregled promjena u gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Podrška za GNOME Shell 49 (zahvaljujući @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Ažuriran njemački prijevod (zahvaljujući @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Pregled promjena u gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Podrška za GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Razdvoji vrijeme provedeno preko ponoći" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Dodan telugu prijevod (zahvaljujući @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Pregled promjena u gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Podrška za GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Dopusti odbacivanje prekrivanja zaslona gestom dok se reproducira " #~ "videozapis" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Dodan gruzijski prijevod (zahvaljujući @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "" #~ "Prilagođeni prijevodi u metapodacima aplikacije (zahvaljujući @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Pregled promjena u gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Popravljeno zadržavanje obavijesti nakon produženja Pomodora" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Pregled promjena u gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Ispravci za GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Prestanak podrške za GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Pregled promjena u gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Podrška za GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Prilagođena skripta izgradnje za meson 0.59.0 (zahvaljujući @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "Neka Pomodoro upravlja obavijestima sustava dok timer radi" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 sekundi" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 sekundi" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuta" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minute" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minute" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minuta" #~ msgid "Timer Ticking" #~ msgstr "Otkucavanje timera" #, fuzzy #~ msgid "Birds" #~ msgstr "Ptice" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;mjerač vremena;vremenski brojač;" #~ msgid "Start/Stop" #~ msgstr "Pokreni/Prekini" #~ msgid "Pause/Resume" #~ msgstr "Zaustavi/Nastavi" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Preskočite na pomodoro ili na pauzu" #~ msgid "Reset current session" #~ msgstr "Ponovno postavi trenutnu sesiju" #~ msgid "Run as background service" #~ msgstr "Pokreni kao uslugu u pozadini" #~ msgid "About Pomodoro" #~ msgstr "O programu Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Jednostavan uslužni program za upravljanja vremenom" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Prekini" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Podrška za GNOME ljusku 3.36" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Neuspjelo aktiviranje proširenja" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Trajanje duge pauze" #~ msgid "A time management utility for GNOME" #~ msgstr "Uslužni program za upravljanje vremenom za GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Uslužni program za GNOME koji pomaže upravljati vremenom pomoću Pomodoro " #~ "tehnike. Namjera programa je poboljšanje produktivnosti i usredotočenosti " #~ "na zadatak pomoću kratkih pauza nakon svakih 25 minuta rada." #~ msgid "Timer window" #~ msgstr "Prozor timera" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indikator za GNOME ljusku" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.24.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.24.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.23.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.23.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.22.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.22.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Updated Brazilian translation (thanks @costaronaldo)" #~ msgstr "Aktualiziran prijevod za katalanski (@antoniofsm)" #, fuzzy #~ msgid "Updated Chinese translation (thanks @HaorongX)" #~ msgstr "Aktualiziran prijevod za kinseski (@wffger)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.21.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.21.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Podrška za GNOME ljusku 3.38 (@ignapk i @szpak)" #, fuzzy #~ msgid "Added Croatian translation (thanks @dayeondev)" #~ msgstr "Dodan prijevod za norveški (@arnotixe)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.20.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Podrška za GNOME ljusku 3.32 (@demokritos)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.2" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Updated Russian translation (@prokoudine)" #~ msgstr "Aktualiziran prijevod za ruski (@rkaverin)" #, fuzzy #~ msgid "Updated Dutch translation (@Vistaus)" #~ msgstr "Aktualiziran prijevod za njemački (@tsabsch)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Podrška samo za GNOME ljusku 3.34" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Podrška za GNOME ljusku 3.36" #, fuzzy #~ msgid "Changed blur effect during break" #~ msgstr "Zamućena pozadina ispod dijaloškog prozora tijekom pauza" #, fuzzy #~ msgid "Added Korean translation (@dayeondev)" #~ msgstr "Dodan prijevod za norveški (@arnotixe)" #, fuzzy #~ msgid "Updated Brazilian translation (@alexandreafa)" #~ msgstr "Aktualiziran prijevod za katalanski (@antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.18.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.18.0" #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Podrška za GNOME ljusku 3.38 (@ignapk i @szpak)" #~ msgid "Removed ayatana-appindicator3 support" #~ msgstr "Uklonjena podrška za ayatana-appindicator3" #~ msgid "Added Norwegian translation (@arnotixe)" #~ msgstr "Dodan prijevod za norveški (@arnotixe)" #~ msgid "Added Finnish translation (@iqqmuT)" #~ msgstr "Dodan prijevod za finski (@iqqmuT)" #~ msgid "Updated Indonesian translation (@atriwidada)" #~ msgstr "Aktualiziran prijevod za indonezijski (@atriwidada)" #~ msgid "Updated Chinese translation (@wffger)" #~ msgstr "Aktualiziran prijevod za kinseski (@wffger)" #~ msgid "Updated Russian translation (@rkaverin)" #~ msgstr "Aktualiziran prijevod za ruski (@rkaverin)" #~ msgid "Updated French translation (@precondition)" #~ msgstr "Aktualiziran prijevod za frankcuski (@precondition)" #~ msgid "Overview of changes in gnome-pomodoro 0.17.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.17.0" #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Podrška za GNOME ljusku 3.36" #~ msgid "Updated Catalan translation (@antoniofsm)" #~ msgstr "Aktualiziran prijevod za katalanski (@antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.16.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.16.0" #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Podrška samo za GNOME ljusku 3.34" #~ msgid "Added esperanto translation (@SeZuo)" #~ msgstr "Dodan prijevod za esperanto (@SeZuo)" #~ msgid "Moved app-menu to main window" #~ msgstr "Izbornik programa premješten je u glavni prozor" #~ msgid "Overview of changes in gnome-pomodoro 0.15.1" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.1" #~ msgid "Minor code cleanups" #~ msgstr "Neki ispravci u programskom kodu" #~ msgid "Overview of changes in gnome-pomodoro 0.15.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.15.0" #~ msgid "Minor code cleanups to support ES6 syntax" #~ msgstr "Neki ispravci u programskom kodu za podršku ES6 sintakse" #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Podrška za GNOME ljusku 3.32 (@demokritos)" #~ msgid "Fix for build with vala 0.44.1 (@snizovtsev)" #~ msgstr "Ispravak za gradnju pomoću vala 0.44.1 (@snizovtsev)" #~ msgid "Updated German translation (@c7hm4r)" #~ msgstr "Aktualiziran prijevod za njemački (@c7hm4r)" #~ msgid "Fix for handle error recreating existing folder (@Rj7)" #~ msgstr "Ispravak greške za ponovnu izradu postojeće mape (@Rj7)" #~ msgid "Overview of changes in gnome-pomodoro 0.14.0" #~ msgstr "Pregled promjena za gnome-pomodoro 0.14.0" #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Podrška za GNOME ljusku 3.28 i 3.30 (@aerostitch)" #~ msgid "Background blur under the dialog during breaks" #~ msgstr "Zamućena pozadina ispod dijaloškog prozora tijekom pauza" #~ msgid "Updated German translation (@tsabsch)" #~ msgstr "Aktualiziran prijevod za njemački (@tsabsch)" #~ msgid "Updated Russian translation (@tigertv)" #~ msgstr "Aktualiziran prijevod za ruski (@tigertv)" #~ msgid "_Timer" #~ msgstr "_Timer" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Tipkovni prečac za uključivanje/isključivanje timera. Promijeni prečac " #~ "upisom novog prečaca." #~ msgid "Pomodoros before a long break" #~ msgstr "Broj Pomodora prije duge pauze" #~ msgid "Keyboard shortcut" #~ msgstr "Tipkovni prečaci" #~ msgid "Screen notifications" #~ msgstr "Ekranske obavijesti" #~ msgid "Wait for activity after a break" #~ msgstr "Čekaj na aktivnost nakon pauze" #~ msgid "Plugins…" #~ msgstr "Dodaci …" #~ msgid "Plugins" #~ msgstr "Dodaci" #~ msgid "Back" #~ msgstr "Natrag" #~ msgid "Complete a few sessions" #~ msgstr "Završi par sesija" #~ msgid "Previous (Alt+Left)" #~ msgstr "Prethodni (Alt+Lijevo)" #~ msgid "Next (Alt+Right)" #~ msgstr "Sljedeći (Alt+Desno)" #~ msgid "Complete" #~ msgstr "Završeno" #~ msgid "Enable" #~ msgstr "Aktiviraj" #~ msgid "Add" #~ msgstr "Dodaj" #~ msgid "Remove" #~ msgstr "Ukloni" #~ msgid "Elapsed Time" #~ msgstr "Proteklo vrijeme" #~ msgid "Pause Timer" #~ msgstr "Zaustavi timer" #~ msgid "Pause break" #~ msgstr "Prekid pauze" #~ msgid "Pause Pomodoro" #~ msgstr "Prekini pomodoro" #~ msgid "Resume break" #~ msgstr "Nastavi pauzu" #~ msgid "Resume Pomodoro" #~ msgstr "Nastavi pomodoro" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "još %d minuta" #~ msgstr[1] "još %d minute" #~ msgstr[2] "još %d minuta" #~ msgid "Report issue" #~ msgstr "Prijavi problem" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Neuspjelo pokretanje usluge %s" #~ msgid "Woodland Birds" #~ msgstr "Šumske ptice" #~ msgid "End of Break Sound" #~ msgstr "Zvuk kraja pauze" #~ msgid "Start of Break Sound" #~ msgstr "Zvuk početka pauze" #~ msgid "Off" #~ msgstr "Isključeno" #~ msgid "Ticking sound" #~ msgstr "Zvuk otkucavanja" #~ msgid "Start of break sound" #~ msgstr "Zvuk početka pauze" #~ msgid "End of break sound" #~ msgstr "Zvuk kraja pauze" #~ msgid "Focus on your task." #~ msgstr "Usredotoči se na zadatak." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Imaš %d minutu" #~ msgstr[1] "Imaš %d minute" #~ msgstr[2] "Imaš %d minuta" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Imaš %d sekundu" #~ msgstr[1] "Imaš %d sekunde" #~ msgstr[2] "Imaš %d sekunda" #~ msgid "Take a longer break" #~ msgstr "Započni dužu pauzu" #~ msgid "Lengthen it" #~ msgstr "Produži" #~ msgid "Shorten it" #~ msgstr "Skrati" #~ msgid "Start pomodoro" #~ msgstr "Pokreni Pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Korištenje tipke „%s” kao prečaca ometat će tipkanje. Pokušaj dodati " #~ "jednu drugu tipku, kao što su Control, Alt ili Shift." #~ msgid "Available" #~ msgstr "Dostupno" #~ msgid "Busy" #~ msgstr "Zauzeto" #~ msgid "Idle" #~ msgstr "Bez aktivnosti" #~ msgid "Invisible" #~ msgstr "Nevidljivo" #, c-format #~ msgid "%d m" #~ msgstr "%d min" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f h" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f h" #~ msgid "Extension is out of date" #~ msgstr "Proširenje je zastarjelo" #~ msgid "Upgrade" #~ msgstr "Nadogradi" focustimerhq-FocusTimer-8581be2/po/id.po000077500000000000000000001616451520625676500202510ustar00rootroot00000000000000# Indonesian translation for focus-timer # Copyright (c) 2017 Free Software Foundation, Inc. # This file is distributed under the same license as the focus-timer package. # # Authors: # Ali , 2017. # Andika Triwidada , 2020, 2025. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 09:26+0200\n" "PO-Revision-Date: 2025-12-23 20:58+0700\n" "Last-Translator: Andika Triwidada \n" "Language-Team: Indonesian\n" "Language: id\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Timer produktivitas yang membantu Anda bekerja lebih efektif dengan membagi " "waktu Anda menjadi sesi kerja terfokus yang diikuti oleh rehat pendek. " "Bekerja selama 25 menit, lalu ambil rehat 5 menit untuk menjaga konsentrasi " "dan mencegah kelelahan kerja (burnout)." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Fitur utama:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Durasi sesi kerja dan rehat yang dapat disesuaikan" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Hamparan layar selama rehat" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Timer" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Statistik harian" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Statistik bulanan" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferensi" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Hamparan layar" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "Mulai atau Berhenti" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "Mulai, Jeda, atau Lanjut" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Mulai Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Mulai" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Berhenti" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Jeda" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Lanjut" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Lewati" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Putar balik" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Perpanjang pomodoro saat ini atau rehat" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Setel ulang" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Tampilkan preferensi" #: src/application.vala:203 msgid "Quit application" msgstr "Keluar aplikasi" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Cetak informasi versi dan keluar" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Bawa ke Fokus" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "tersisa %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "Aksi ubahan \"%s\" gagal" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Batas waktu tercapai" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "Gagal menjalankan perintah" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "Perintah kosong" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Tanda kutip belum ditutup" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Perintah tidak valid" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Variabel \"%s\" tidak dikenal" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Format \"%s\" tidak dikenal" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Program \"%s\" tidak ditemukan" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Aksi" #: src/core/event.vala:183 msgid "Countdown" msgstr "Hitung mundur" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Sesi" #: src/core/event.vala:189 msgid "Other" msgstr "Lainnya" #: src/core/event.vala:269 msgid "Started the timer." msgstr "Memulai timer." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "Menghentikan timer secara manual." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Hitung mundur telah dijeda secara manual. Tidak dipicu saat mengunci layar " "atau saat sistem disuspensi." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Hitung mundur telah dilanjutkan secara manual." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "Melompat ke blok waktu berikutnya sebelum hitung mundur selesai." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Aksi putar balik telah digunakan. Ini menambahkan jeda pada waktu lampau." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Membersihkan sesi secara manual." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Selesai" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Hitung mundur telah selesai. Jika menunggu konfirmasi, durasi blok waktu " "masih dapat diubah." #: src/core/event.vala:333 msgid "Changed" msgstr "Berubah" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "Dipicu pada setiap perubahan yang terkait dengan hitung mundur." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Konfirmasi Kemajuan" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "Konfirmasi manual diperlukan untuk memulai blok waktu berikutnya." #: src/core/event.vala:350 msgid "Advanced" msgstr "Tingkat Lanjut" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Beralih atau dilewati ke blok waktu berikutnya." #: src/core/event.vala:358 msgid "State Changed" msgstr "Keadaan Berubah" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Beralih ke blok waktu berikutnya atau ketika rehat diberi label ulang." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Dijadwalkan ulang" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Dipicu ketika blok waktu yang dijadwalkan telah berubah." #: src/core/event.vala:374 msgid "Expired" msgstr "Kedaluwarsa" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "Dipicu ketika sesi akan disetel ulang karena tidak ada aktivitas." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Waktunya rehat" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Rehat sejenak" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Rehat panjang" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro akan berakhir" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "Waktunya Rehat" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Rehat akan berakhir" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 menit" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Siap-siap…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Pomodoro telah usai!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "Rehat telah usai!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Konfirmasi awal Pomodoro…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Konfirmasi awal rehat…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Konfirmasi awal rehat pendek…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Konfirmasi awal rehat panjang…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Lewati Rehat" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Gagal menginisialisasi pemutaran" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Berkas tidak ditemukan" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Tipe berkas tidak didukung" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "Dihentikan" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Rehat" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Rehat Pendek" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Rehat Panjang" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%uj" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u jam" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u menit" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u detik" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Waktu tepat dari acara saat ini." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Fase siklus Pomodoro saat ini. Nilai yang mungkin: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status blok waktu saat ini. Nilai yang mungkin: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Tanda yang menunjukkan apakah hitung mundur telah dimulai." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Tanda yang menunjukkan apakah hitung mundur dijeda." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Tanda yang menunjukkan apakah hitung mundur telah selesai." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Tanda yang menunjukkan apakah timer sedang aktif menghitung mundur." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Durasi hitung mundur saat ini." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Perbedaan antara waktu yang telah berlalu dan waktu sebenarnya." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "Banyaknya waktu yang dihabiskan pada hitung mundur." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "Banyaknya waktu yang tersisa sebelum hitung mundur berakhir." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Waktu saat hitung mundur dimulai." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "Ekstensi GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Dapatkan pengalaman terbaik!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Aktifkan ekstensi GNOME Shell untuk integrasi desktop yang mulus" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Selalu dalam jangkauan" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "Kontrol timer langsung dari bilah atas tanpa membuka aplikasi" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Kurangi gangguan" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Pengingat rehat yang lebih baik" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Hamparan layar penuh yang elegan, yang membuat istirahat menjadi pengalaman " "yang lebih menyenangkan" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Siap mencobanya?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "Pasang Ekstens_i" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_Nanti Saja" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Terjadi kesalahan" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Salin ke papan klip" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Coba Lagi" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "Gugurk_an" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Batas waktu tercapai" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Pemasangan ekstensi tidak diizinkan" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "Gagal mengunduh ekstensi" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Hamparan Layar" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Desktop" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "Ekstensi GNOME Shell tersedia" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Pelajari Selengkapnya" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "Tidak digunakan" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Selesai!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistik" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Keluar" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Log" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Log Kosong" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Entri akan muncul di sini setelah Anda memulai timer." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Konteks" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Perintah" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Keluaran" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Galat" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Kode Keluar:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Waktu Eksekusi:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "Ali , 2017\n" "Andika Triwidada , 2020, 2025" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Donasi" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "Rehat" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Gangguan" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "Rasio Rehat" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Hari" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Minggu" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Bulan" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "Belum ada yang dapat dilihat di sini" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Selesaikan beberapa Pomodoro untuk mengisi bagian ini!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Melewati %u hari" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Melewati %u minggu" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Melewati %u bulan" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Hari ini" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Kemarin" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Minggu Ini" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "Minggu %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Minggu %u dari %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Rehat Pende_k" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Rehat Panjan_g" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_Rehat" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Buka hamparan layar" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Sesi telah kedaluwarsa" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "Rehat panjang dalam %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "Putar balik satu menit" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "Tampilan _Ringkas" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferensi" #: src/ui/main/window.ui:19 msgid "_About" msgstr "Tent_ang" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Keluar" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Menu Utama" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Biarkan timer berjalan?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Anda dapat membiarkannya berjalan di latar belakang — notifikasi dan " "pintasan papan ketik akan tetap berfungsi." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "Jalankan di latar belakang" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Waktunya rehat" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "Jendela Utama" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Lebih Suka Tema Gelap" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Lebih Suka Tampilan Ringkas" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "Dimulai" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Dijeda" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "Sunting Aksi Ubahan" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Batal" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Simpan" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nama" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "Pemicu" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Acara" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Jalankan perintah setelah suatu acara." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Kondisi" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "Pastikan eksekusi perintah kedua setelah kondisi tidak lagi terpenuhi." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Acara" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Tambah _Acara" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filter" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "Perintah Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "Perintah" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Perintah Saat Kondisi Terpenuhi" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Perintah Saat Kondisi Tidak Terpenuhi" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Direktori Kerja" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Gunakan Subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Jalankan program dari subshell seperti sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Kirim Data Masukan" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "Sebagai ganti mengirim variabel, Anda dapat memroses objek JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Tunggu Selesai" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Blokir eksekusi perintah lain sampai perintah ini selesai." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "_Hapus Aksi" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Belum ada acara yang ditentukan." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "Tambah Aksi Ubahan" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "T_ambah" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Pilih Direktori Kerja" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Pilih" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "Aksi tanpa judul" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Tambah Kondisi" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Tambah Grup" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "DAN" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ATAU" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Adalah" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Bukan" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Sama Dengan" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Lebih Dari" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Kurang Dari" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Ya" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Tidak" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "Menit" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Detik" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Jam" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Pilih Bidang…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Keadaan" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Berjalan" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "Durasi" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Sisipkan Variabel" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Log" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Tampilkan log eksekusi" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Jalankan perintah shell secara otomatis pada acara timer atau kondisi " "tertentu. Pelajari selengkapnya." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "Atur Pintasan" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Atur" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Dinonaktifkan" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Tekan Esc untuk membatalkan atau Backspace untuk menonaktifkan " "pintasan papan ketik" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Pintasan global memungkinkan Anda mengontrol aplikasi bahkan saat tidak ada " "di layar. Pintasan ini berfungsi selama aplikasi berjalan di latar belakang." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Buka pengaturan aplikasi untuk menyunting pintasan global" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_Sunting" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Masukkan pintasan baru untuk memulai atau menghentikan timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Masukkan pintasan baru untuk memulai/menjeda/melanjutkan timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Masukkan pintasan baru untuk memulai timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Masukkan pintasan baru untuk menghentikan timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Masukkan pintasan baru untuk menjeda timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Masukkan pintasan baru untuk melanjutkan timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Masukkan pintasan baru untuk melewati" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Putar Balik Satu Menit" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Masukkan pintasan baru untuk memutar balik" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Masukkan pintasan baru untuk membawa jendela ke fokus" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Pengumuman" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Waktu Hampir Habis" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "Beri tahu saat Pomodoro atau rehat akan berakhir." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Notifikasi layar penuh yang ditujukan untuk memaksa pengambilan rehat." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Tunda Kunci" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Periode tanpa aktivitas sebelum mengunci layar." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Tunda Buka Kembali" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Periode tanpa aktivitas untuk membuka kembali hamparan setelah ditutup." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Tidak Pernah" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notifikasi" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Suara" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Penampilan" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "Pintasan Papan Ketik" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "Otomatisasi" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Suara Dinonaktifkan" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "Suara Peringatan" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Suara Pomodoro Selesai" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "Suara Rehat Selesai" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "Suara Latar Belakang" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Bel" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Bel Kencang" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Detak Jam" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Nihil" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volume:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Pilih Suara Ubahan" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Durasi Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "Durasi Rehat Pendek" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "Durasi Rehat Panjang" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Jumlah Siklus" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Perilaku" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Jeda Dengan Mengunci Layar" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Konfirmasi Memulai Rehat" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Konfirmasi Memulai Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Satu sesi tunggal akan memakan waktu %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% waktu akan dialokasikan untuk rehat." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Terapkan perubahan pada Pomodoro yang sedang berjalan?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Terapkan perubahan pada rehat yang sedang berjalan?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "Terapkan" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Bilah Sisi" #~ msgid "Time management utility" #~ msgstr "Utilitas manajemen waktu" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Menjaga fokus dengan mengambil rehat secara berkala" #~ msgid "Visual and audio notifications" #~ msgstr "Notifikasi visual dan audio" #~ msgid "Time tracking and statistics" #~ msgstr "Pelacakan waktu dan statistik" #~ msgid "GNOME desktop integration" #~ msgstr "Integrasi desktop GNOME" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Jalankan perintah ubahan setelah Pomodoro atau rehat" #~ msgid "15 seconds" #~ msgstr "15 detik" #~ msgid "30 seconds" #~ msgstr "30 detik" #~ msgid "1 minute" #~ msgstr "1 menit" #~ msgid "2 minutes" #~ msgstr "2 menit" #~ msgid "3 minutes" #~ msgstr "3 menit" #~ msgid "5 minutes" #~ msgstr "5 menit" #~ msgid "Compact timer" #~ msgstr "Timer ringkas" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.28.1" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Menambahkan terjemahan Tamil (terima kasih @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Menambahkan terjemahan Ibrani (terima kasih @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.28.0" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Dukungan untuk GNOME Shell 49 (terima kasih @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Pembaruan terjemahan Jerman (terima kasih @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.27.0" #~ msgid "Support for GNOME Shell 48" #~ msgstr "Dukungan untuk GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Pisahkan waktu yang dihabiskan melewati tengah malam" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Menambahkan terjemahan Telugu (terima kasih @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.26.0" #~ msgid "Support for GNOME Shell 47" #~ msgstr "Dukungan untuk GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Izinkan untuk menutup hamparan layar dengan gestur saat video diputar" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Menambahkan terjemahan Georgia (terima kasih @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Penyesuaian terjemahan dalam appdata (terima kasih @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Perbaikan notifikasi yang menetap setelah memperpanjang Pomodoro" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.25.1" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Perbaikan untuk GNOME Shell 46" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Hentikan dukungan untuk GNOME Shell 45" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Ringkasan perubahan di gnome-pomodoro 0.25.0" #~ msgid "Support for GNOME Shell 46" #~ msgstr "Dukungan untuk GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Sesuaikan skrip build ke meson 0.59.0 (terima kasih @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Biarkan Pomodoro mengelola notifikasi sistem saat timer berjalan" #~ msgid "Timer Ticking" #~ msgstr "Detak Timer" #~ msgid "Birds" #~ msgstr "Burung" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;pewaktu;pengatur waktu;" #~ msgid "Start/Stop" #~ msgstr "Mulai/Berhenti" #~ msgid "Pause/Resume" #~ msgstr "Jeda/Lanjut" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Lewati ke pomodoro atau rehat" #~ msgid "Reset current session" #~ msgstr "Setel ulang sesi saat ini" #~ msgid "Run as background service" #~ msgstr "Jalankan sebagai layanan latar belakang" #~ msgid "About Pomodoro" #~ msgstr "Tentang Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Suatu utilitas simpel manajemen waktu" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Berhenti" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Gagal mengaktifkan extension" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Durasi rehat lama" #~ msgid "A time management utility for GNOME" #~ msgstr "Suatu utilitas manajemen waktu untuk GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Seuatu utilitas GNOME yang mana membantu mengatur waktu menurut Teknik " #~ "Pomodoro. Hal ini bermaksud untuk menambah fokus beserta produktifitas " #~ "melalui mengambilan jeda istirahat sejenak setiap 25 menit dari pekerjaan." #~ msgid "Timer window" #~ msgstr "Jendela pengatur waktu" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Indikator untuk GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Indikator untuk GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Timer" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Pintasan papan ketik untuk menjungkit timer. Untuk mengubah, masukan " #~ "pintasan baru." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoro sebelum rehat lama" #~ msgid "Keyboard shortcut" #~ msgstr "Pintasan papan ketik" #~ msgid "Screen notifications" #~ msgstr "Notifikasi layar" #~ msgid "Wait for activity after a break" #~ msgstr "Tunggu aktifitas setelah rehat" #~ msgid "Plugins…" #~ msgstr "Pengaya…" #~ msgid "Plugins" #~ msgstr "Plugins" #~ msgid "Back" #~ msgstr "Kembali" #~ msgid "Complete a few sessions" #~ msgstr "Selesaikan beberapa sesi" #~ msgid "Previous (Alt+Left)" #~ msgstr "Sebelumnya (Alt+Kiri)" #~ msgid "Next (Alt+Right)" #~ msgstr "Berikutnya (Alt+Kanan)" #~ msgid "Complete" #~ msgstr "Komplit" #~ msgid "Enable" #~ msgstr "Aktifkan" #~ msgid "Add" #~ msgstr "Tambahkan" #~ msgid "Remove" #~ msgstr "Hapus" #~ msgid "Elapsed Time" #~ msgstr "Waktu Berlalu" #~ msgid "Pause Timer" #~ msgstr "Timer Jeda" #~ msgid "Pause break" #~ msgstr "Jeda" #~ msgid "Pause Pomodoro" #~ msgstr "Jeda" #~ msgid "Resume break" #~ msgstr "Lanjut" #~ msgid "Resume Pomodoro" #~ msgstr "Lanjut" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d menit tersisa" #~ msgstr[1] "%d menit tersisa" #~ msgid "Report issue" #~ msgstr "Laporkan isu" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Gagal menjalankan layanan %s" #~ msgid "Woodland Birds" #~ msgstr "Burung Woodland" #~ msgid "End of Break Sound" #~ msgstr "Suara Akhir Rehat" #~ msgid "Start of Break Sound" #~ msgstr "Suara Awal Rehat" #~ msgid "Off" #~ msgstr "Mati" #~ msgid "Ticking sound" #~ msgstr "Suara Detak" #~ msgid "Start of break sound" #~ msgstr "Suara awal rehat" #~ msgid "End of break sound" #~ msgstr "Suara akhir rehat" #~ msgid "Focus on your task." #~ msgstr "Fokus pada tugas Anda." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Kamu memiliki %d menit" #~ msgstr[1] "Kamu memiliki %d menit" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Kamu memiliki %d detik" #~ msgstr[1] "Kamu memiliki %d detik" #~ msgid "Take a longer break" #~ msgstr "Ambil rehat yang lebih lama" #~ msgid "Lengthen it" #~ msgstr "Perpanjang" #~ msgid "Shorten it" #~ msgstr "Perpendek" #~ msgid "Start pomodoro" #~ msgstr "Mulai pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Menggunakan \"%s\" sebagai pintasan yang akan mengganggu pengetikan. Coba " #~ "tambahkan tombol lainnya seperti Control, Alt, atau Shift." #~ msgid "Available" #~ msgstr "Ada" #~ msgid "Busy" #~ msgstr "Sibuk" #~ msgid "Idle" #~ msgstr "Mengganggur" #~ msgid "Invisible" #~ msgstr "Tak tampak" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f j" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f j" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~| msgid "State" #~ msgid "_Stats" #~ msgstr "_Statistik" #~ msgid "It seems to be uninstalled" #~ msgstr "Nampaknya tidak dipasang" #~ msgid "Extension is out of date" #~ msgstr "Extension sudah kedaluwarsa" #~ msgid "Upgrade" #~ msgstr "Tingkatkan" focustimerhq-FocusTimer-8581be2/po/it.po000066400000000000000000001720101520625676500202520ustar00rootroot00000000000000# Italian translation for focus-timer # Copyright (c) 2017 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Giuseppe Pignataro , 2017. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 12:11+0100\n" "Last-Translator: Giuseppe Pignataro (Fastbyte01) \n" "Language-Team: Italian\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Un timer per la produttività che ti aiuta a lavorare in modo più efficace " "suddividendo il tempo in sessioni di lavoro focalizzato seguite da brevi " "pause. Lavora per 25 minuti, poi fai una pausa di 5 minuti per mantenere la " "concentrazione e prevenire il burnout." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Caratteristiche principali:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Durata delle sessioni di lavoro e delle pause personalizzabile" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Overlay a schermo durante le pause" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Timer" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Statistiche giornaliere" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Statistiche mensili" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferenze" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Overlay a schermo" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Avvia o ferma" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Avvia, metti in pausa o riprendi" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Avvia Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Avvia" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stop" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pausa" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Riprendi" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Salta" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Riavvolgi" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Estendi l'attuale pomodoro o pausa" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Ripristina" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Mostra preferenze" #: src/application.vala:203 msgid "Quit application" msgstr "Chiudi applicazione" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Stampa informazioni di versione e esce" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Porta in primo piano" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s rimanenti" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Azione personalizzata \"%s\" non riuscita" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Tempo limite raggiunto" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Impossibile eseguire il comando" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Il comando è vuoto" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Virgolette non chiuse" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Comando non valido" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Variabile \"%s\" sconosciuta" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Formato \"%s\" sconosciuto" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programma \"%s\" non trovato" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Azioni" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Conto alla rovescia" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sessione" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Altro" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Timer avviato." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Il timer è stato fermato manualmente." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Il conto alla rovescia è stato messo in pausa manualmente. Non si attiva " "bloccando lo schermo o sospendendo il sistema." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Il conto alla rovescia è stato ripreso manualmente." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "" "Passato al blocco temporale successivo prima del termine del conto alla " "rovescia." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "L'azione di riavvolgimento è stata utilizzata. Aggiunge una pausa nel " "passato." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Sessione azzerata manualmente." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Terminato" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Il conto alla rovescia è terminato. In attesa di conferma, la durata del " "blocco temporale può ancora essere modificata." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Cambiato" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Attivato su qualsiasi modifica relativa al conto alla rovescia." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Conferma avanzamento" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "" "È richiesta una conferma manuale per avviare il blocco temporale successivo." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Avanzato" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Passato o saltato al blocco temporale successivo." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Stato cambiato" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" "Passato a un blocco temporale successivo o quando una pausa viene rinominata." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Riprogrammato" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Attivato quando i blocchi temporali pianificati sono cambiati." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Scaduto" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "Attivato quando la sessione sta per essere ripristinata per inattività." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Fai una pausa" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Fai una breve pausa" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Fai una lunga pausa" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Il Pomodoro sta per finire" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Fai una pausa" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "La pausa sta per finire" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuto" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Preparati…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Il Pomodoro è finito!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "La pausa è finita!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Conferma l'inizio di un Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Conferma l'inizio di una pausa…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Conferma l'inizio di una pausa breve…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Conferma l'inizio di una pausa lunga…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Salta pausa" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Inizializzazione riproduzione non riuscita" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "File non trovato" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Tipo di file non supportato" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Fermato" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pausa" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Pausa breve" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Pausa lunga" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u ora" msgstr[1] "%u ore" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minuti" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u secondo" msgstr[1] "%u secondi" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "L'ora esatta dell'evento corrente." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "La fase corrente del ciclo Pomodoro. Valori possibili: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Stato del blocco temporale corrente. Valori possibili: scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Un flag che indica se il conto alla rovescia è iniziato." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Un flag che indica se il conto alla rovescia è in pausa." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Un flag che indica se il conto alla rovescia è terminato." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Un flag che indica se il timer è attivo." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Durata del conto alla rovescia corrente." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Discrepanza tra tempo trascorso e tempo passato." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "La quantità di tempo spesa per il conto alla rovescia." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Il tempo rimasto prima del termine del conto alla rovescia." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "L'ora in cui è iniziato il conto alla rovescia." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "Estensione GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Ottieni l'esperienza migliore!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Abilita l'estensione GNOME Shell per una perfetta integrazione con il " "desktop" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Sempre a portata di mano" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "" "Controlla il timer direttamente dalla barra superiore senza aprire l'app" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Meno distrazioni" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Promemoria delle pause perfezionati" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegante overlay a tutto schermo che rende le pause un'esperienza più " "piacevole" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Pronto a provarlo?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Installa estensione" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Non ora" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Qualcosa è andato storto" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Copia negli appunti" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Riprova" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Interrompi" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Tempo limite raggiunto" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "L'installazione di estensioni non è consentita" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Download dell'estensione non riuscito" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Overlay a schermo" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Desktop" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "Estensione GNOME Shell disponibile" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Scopri di più" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Inutilizzato" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Terminato!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistiche" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Esci" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Log" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Log vuoto" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Le voci appariranno qui una volta avviato il timer." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Contesto" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Comando" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Output" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Errore" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Codice di uscita:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Tempo di esecuzione:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Giuseppe Pignataro (Fastbyte01) " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Dona" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pause" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Interruzioni" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Rapporto pause" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Giorno" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Settimana" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mese" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Non c'è ancora nulla da vedere" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Completa alcuni Pomodoro per riempire questa sezione!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Saltato %u giorno" msgstr[1] "Saltati %u giorni" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Saltata %u settimana" msgstr[1] "Saltate %u settimane" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Saltato %u mese" msgstr[1] "Saltati %u mesi" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Oggi" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Ieri" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Questa settimana" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Settimana %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Settimana %u di %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Pausa _Breve" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Pausa _Lunga" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pausa" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Apri overlay a schermo" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "La sessione è scaduta" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Pausa lunga tra %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Riavvolgi di un minuto" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "Vista _compatta" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferenze" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Info su" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Esci" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Menu principale" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Mantenere il timer in esecuzione?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Puoi mantenerlo in esecuzione in background — le notifiche e le scorciatoie " "da tastiera continueranno a funzionare." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Esegui in background" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "È tempo di fare una pausa" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Finestra principale" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Preferisci il tema scuro" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Preferisci la vista compatta" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Iniziato" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "In pausa" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Modifica azione personalizzata" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Annulla" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Salva" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nome" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Trigger" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Evento" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Esegui comando dopo un evento." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Condizione" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Assicura l'esecuzione di un secondo comando quando la condizione non è più " "soddisfatta." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Eventi" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Aggiungi _evento" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filtra" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filtro" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Comando Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Comandi" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Comando condizione soddisfatta" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Comando condizione non soddisfatta" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Directory di lavoro" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Usa subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Esegui il programma da una subshell come sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Passa dati di input" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Invece di passare variabili puoi elaborare un oggetto JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Attendi completamento" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Blocca l'esecuzione di altri comandi fino al termine del comando." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Elimina azione" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Nessun evento ancora specificato." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Aggiungi azione personalizzata" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Aggiungi" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Seleziona directory di lavoro" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Seleziona" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Azione senza titolo" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Aggiungi condizione" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Aggiungi gruppo" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "AND" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "OR" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "È" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Non è" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Uguale a" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Maggiore di" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Minore di" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Sì" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "No" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuti" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Secondi" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Ore" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Seleziona campo…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Stato" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "In esecuzione" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Durata" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Inserisci variabile" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Formato" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Log" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Mostra log di esecuzione" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Esegui comandi shell automaticamente su eventi del timer o condizioni. Scopri di più." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Imposta scorciatoia" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Imposta" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Disabilitato" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Premi Esc per annullare o Backspace per disabilitare la " "scorciatoia da tastiera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Le scorciatoie globali ti permettono di controllare l'app anche quando non è " "sullo schermo. Funzionano finché l'app è in esecuzione in background." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Apri le impostazioni dell'app per modificare le scorciatoie globali" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Modifica" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Inserisci una nuova scorciatoia per avviare o fermare il timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Inserisci una nuova scorciatoia per avviare/mettere in pausa/riprendere il " "timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Inserisci una nuova scorciatoia per avviare il timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Inserisci una nuova scorciatoia per fermare il timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Inserisci una nuova scorciatoia per mettere in pausa il timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Inserisci una nuova scorciatoia per riprendere il timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Inserisci una nuova scorciatoia per saltare" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Riavvolgi di un minuto" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Inserisci una nuova scorciatoia per riavvolgere" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Inserisci una nuova scorciatoia per portare la finestra in primo piano" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Annunci" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Tempo in esaurimento" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Notifica quando il Pomodoro o la pausa stanno per finire." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "" "Una notifica a schermo intero pensata per incoraggiare a fare una pausa." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Ritardo blocco" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Periodo di inattività prima di bloccare lo schermo." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Ritardo riapertura" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Periodo di inattività per riaprire l'overlay dopo essere stato chiuso." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Mai" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notifiche" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Suoni" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Aspetto" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Scorciatoie da tastiera" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automazione" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "I suoni sono disabilitati" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Suoni di avviso" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Suono Pomodoro terminato" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Suono pausa terminata" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Suono di sottofondo" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Campanello" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Campanello forte" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Ticchettio dell'orologio" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Nessuno" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volume:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Seleziona suono personalizzato" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Durata Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Durata pausa breve" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Durata pausa lunga" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Numero di cicli" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Comportamento" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pausa al blocco dello schermo" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Conferma inizio pausa" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Conferma inizio Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Una singola sessione richiederà %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "Il %u%% del tempo sarà allocato per le pause." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Applica le modifiche al Pomodoro in corso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Applica le modifiche alla pausa in corso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Applica" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Barra laterale" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Utility per la gestione del tempo" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Mantieni la concentrazione facendo pause frequenti" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Notifiche visive e sonore" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Monitoraggio del tempo e statistiche" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integrazione con il desktop GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Esegui comandi personalizzati dopo un Pomodoro o una pausa" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Timer compatto" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Aggiunta traduzione in Tamil (grazie a @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Aggiunta traduzione in Ebraico (grazie a @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Supporto per GNOME Shell 49 (grazie a @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Aggiornata traduzione in Tedesco (grazie a @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Supporto per GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Suddividi il tempo trascorso oltre la mezzanotte" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Aggiunta traduzione in Telugu (grazie a @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Supporto per GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Consenti di chiudere l'overlay con una gesture quando un video è in " #~ "riproduzione" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Aggiunta traduzione in Georgiano (grazie a @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Regolate le traduzioni in appdata (grazie a @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "" #~ "Risolto il problema della notifica persistente dopo l'estensione del " #~ "Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Correzioni per GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Terminato il supporto per GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Panoramica delle modifiche in gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Supporto per GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Adattato lo script di build a meson 0.59.0 (grazie a @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Lascia che Pomodoro gestisca le notifiche di sistema mentre il " #~ "timer è attivo" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 secondi" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 secondi" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuto" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minuti" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minuti" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minuti" #~ msgid "Timer Ticking" #~ msgstr "Ticchettio del timer" #, fuzzy #~ msgid "Birds" #~ msgstr "Uccelli" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Avvia/Stop" #~ msgid "Pause/Resume" #~ msgstr "Pausa/Riprendi" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Passa a un pomodoro o a una pausa" #~ msgid "Reset current session" #~ msgstr "Reimposta la sessione corrente" #~ msgid "Run as background service" #~ msgstr "Esegui come servizio in background" #~ msgid "About Pomodoro" #~ msgstr "Info su Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Una semplice utility per la gestione del tempo" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Stop" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Impossibile abilitare l'estensione" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Durata pausa lunga" #~ msgid "A time management utility for GNOME" #~ msgstr "Una utility per la gestione del tempo per GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Un utility per GNOME che ti aiuta a gestire il tempo secondo la Tecnica " #~ "Pomodoro. Questa è fatta per migliorare la tua produttività e la tua " #~ "concentrazione facendo delle brevi pausa ogni 25 minuti di lavoro." #~ msgid "Timer window" #~ msgstr "Finestra del timer" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Indicatore per la GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Indicatore per la GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Timer" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Scorciatoia da tastiera per gestire il timer. Inserisci una nuova " #~ "scorciatoia per cambiarla." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoro prima di una pausa lunga" #~ msgid "Keyboard shortcut" #~ msgstr "Scorciatoie da tastiera" #~ msgid "Screen notifications" #~ msgstr "Notifiche a schermo" #~ msgid "Wait for activity after a break" #~ msgstr "Rimane in attesa di attività dopo una pausa" #~ msgid "Plugins…" #~ msgstr "Plugin…" #~ msgid "Plugins" #~ msgstr "Plugin" #~ msgid "Back" #~ msgstr "Indietro" #~ msgid "Complete a few sessions" #~ msgstr "Completa alcune sessioni" #~ msgid "Previous (Alt+Left)" #~ msgstr "Precedente (Alt+Sinistra)" #~ msgid "Next (Alt+Right)" #~ msgstr "Successivo (Alt+Destra)" #~ msgid "Complete" #~ msgstr "Completo" #~ msgid "Enable" #~ msgstr "Abilita" #~ msgid "Add" #~ msgstr "Aggiungi" #~ msgid "Remove" #~ msgstr "Rimuovi" #~ msgid "Elapsed Time" #~ msgstr "Tempo trascorso" #~ msgid "Pause Timer" #~ msgstr "Pausa timer" #~ msgid "Pause break" #~ msgstr "Pausa di interruzione" #~ msgid "Pause Pomodoro" #~ msgstr "Interrompere il pomodoro" #~ msgid "Resume break" #~ msgstr "Continua pausa" #~ msgid "Resume Pomodoro" #~ msgstr "Continua pomodoro" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minuto rimanente" #~ msgstr[1] "%d minuti rimanenti" #~ msgid "Report issue" #~ msgstr "Segnala problemi" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Impossibile avviare il servizio %s" #~ msgid "Woodland Birds" #~ msgstr "Uccelli di bosco" #~ msgid "End of Break Sound" #~ msgstr "Suono fine pausa" #~ msgid "Start of Break Sound" #~ msgstr "Suono inizio pausa" #~ msgid "Off" #~ msgstr "Off" #~ msgid "Ticking sound" #~ msgstr "Suono ticchettio" #~ msgid "Start of break sound" #~ msgstr "Suono inizio pausa" #~ msgid "End of break sound" #~ msgstr "Suono fine pausa" #~ msgid "Focus on your task." #~ msgstr "Metti a fuoco le tue attività." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Hai %d minuto" #~ msgstr[1] "Hai %d minuti" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Hai %d secondo" #~ msgstr[1] "Hai %d secondi" #~ msgid "Take a longer break" #~ msgstr "Fai una pausa molto lunga" #~ msgid "Lengthen it" #~ msgstr "Allungala" #~ msgid "Shorten it" #~ msgstr "Accorciala" #~ msgid "Start pomodoro" #~ msgstr "Avvia Pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Usare \"%s\" come scorciatoia potrebbe interferire con la digitazione. " #~ "Prova ad aggiungere un altro tasto, come Control, Alt o Shift." #~ msgid "Available" #~ msgstr "Disponibile" #~ msgid "Busy" #~ msgstr "Occupato" #~ msgid "Idle" #~ msgstr "Inattivo" #~ msgid "Invisible" #~ msgstr "Invisibile" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f o" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f o" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "Remind to take a break" #~ msgstr "Ricorda di fare una pausa" #, javascript-format #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d nuovo messaggio" #~ msgstr[1] "%d nuovi messaggi" #~ msgid "Take a break!" #~ msgstr "Fai una pausa!" #, javascript-format #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Hai %d minuto prima del tuo prossimo pomodoro." #~ msgstr[1] "Hai %d minuti prima del tuo prossimo pomodoro." #, javascript-format #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Hai %d secondo prima del tuo prossimo pomodoro." #~ msgstr[1] "Hai %d secondi prima del tuo prossimo pomodoro." #~ msgid "Hey!" #~ msgstr "Hey!" #~ msgid "You're missing out on a break" #~ msgstr "Ti stai perdendo una pausa" #~ msgid "It seems to be uninstalled" #~ msgstr "Sembra che non sia installato" #~ msgid "Extension is out of date" #~ msgstr "L'estensione non è aggiornata" #~ msgid "Upgrade" #~ msgstr "Aggiorna" #~ msgid "Remove Sound" #~ msgstr "Rimuovi suono" focustimerhq-FocusTimer-8581be2/po/ka.po000066400000000000000000002016241520625676500202350ustar00rootroot00000000000000# Georgian translation for focus-timer # Copyright (c) 2024 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Temuri Doghonadze , 2024-2026. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 09:26+0200\n" "PO-Revision-Date: 2026-01-10 15:43+0100\n" "Last-Translator: Temuri Doghonadze \n" "Language-Team: Georgian\n" "Language: ka\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "Focus Timer" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "პროდუქტიულობის ტაიმერი, რომელიც დაგეხმარებათ უფრო ეფექტურად მუშაობაში. ის " "სამუშაო დროს ყოფს ფოკუსირებულ სესიებად და მოკლე შესვენებებად. იმუშავეთ 25 " "წუთი, შემდეგ კი შეისვენეთ 5 წუთით კონცენტრაციის შესანარჩუნებლად." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "ძირითადი ფუნქციები:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "სამუშაო სესიის და შესვენების ხანგრძლივობის მორგება" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "ეკრანზე განლაგება შესვენებების დროს" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "წამმზომი" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "დღიური სტატისტიკა" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "ყოველთვიური სტატისტიკა" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "მორგება" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "ეკრანზე განლაგება" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "გაშვება ან გაჩერება" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "გაშვება, შეჩერება, ან გაგრძელება" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Pomodoro-ის გაშვება" #: src/application.vala:164 msgid "Start break" msgstr "შესვენების დაწყება" #: src/application.vala:167 msgid "Start short break" msgstr "მოკლე დასვენების დასაწყისი" #: src/application.vala:170 msgid "Start long break" msgstr "გრძელი დასვენების დასაწყისი" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "გაშვება" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "გაჩერება" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "პაუზა" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "გაგრძელება" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "გამოტოვება" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "გადახვევა" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "წამი" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "მიმდინარე პომოდოროს ან შესვენების გაფართოება" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "ჩამოყრა" #: src/application.vala:197 msgid "Print timer status" msgstr "ტაიმერის სტატუსის გამოტანა" #: src/application.vala:200 msgid "Show preferences" msgstr "პარამეტრების _ჩვენება" #: src/application.vala:203 msgid "Quit application" msgstr "აპლიკაციიდან გასვლა" #: src/application.vala:206 msgid "Print version information and exit" msgstr "ვერსიის ჩვენება და გასვლა" #: src/application.vala:240 msgid "Timer Options:" msgstr "ტაიმერის მორგება:" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "ტაიმერის მართვის პარამეტრების ჩვენება" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "შეცდომების შესახებ მოგვწერეთ მისამართზე: %s" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "ფოკუსში მოყვანა" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "დარჩენილია %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" "არასწორი გამოყენება. ერთდროულად შესაძლებელია ტაიმერის მართვის, მხოლოდ, ერთი " "ალმის გადაცემა." #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "მომხმარებლის ქმედება \"%s\" ჩავარდა" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "მოლოდინის ვადა ამოიწურა" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "ბრძანების შესრულება ჩავარდა" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "ბრძანება ცარიელია" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "დაუხურავი ბრჭყალი" #: src/core/command.vala:515 msgid "Invalid command" msgstr "არასწორი ბრძანება" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "უცნობი ცვლადი \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "უცნობი ფორმატი \"%s\"" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "პროგრამა \"%s\" აღმოჩენილი არაა" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "ქმედებები" #: src/core/event.vala:183 msgid "Countdown" msgstr "ათვლა" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "სესია" #: src/core/event.vala:189 msgid "Other" msgstr "სხვა" #: src/core/event.vala:269 msgid "Started the timer." msgstr "ტაიმერი გაეშვა." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "ტაიმერი ხელით გააჩერეს." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "ათვლა ხელით შეჩერდა. არ მოქმედებს ეკრანის დაბლოკვისას ან სისტემის პროგრამული " "ძილისას." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "ათვლა ხელით გაგრძელდა." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "შემდეგ ბლოკზე გადასვლა ათვლის დასრულებამდე." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "გამოყენებულია გადახვევის ქმედება. ის ამატებს პაუზას წარსულში." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "სესია ხელით გასუფთავდა." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "დასრულდა" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "ათვლა დასრულდა. თუ დადასტურებას ელოდებით, დროის ბლოკის ხანგრძლივობა ჯერ " "კიდევ შეიძლება, შეიცვალოს." #: src/core/event.vala:333 msgid "Changed" msgstr "შეცვლილია" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "ირთვება უკუათვლასთან დაკავშირებული ნებისმიერი ცვლილებისას." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "წინსვლის დადასტურება" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "შემდეგი დროის ბლოკის დასაწყებად საჭიროა ხელით დადასტურება." #: src/core/event.vala:350 msgid "Advanced" msgstr "დამატებით" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "გადასვლა ან გამოტოვება შემდეგ დროის ბლოკზე." #: src/core/event.vala:358 msgid "State Changed" msgstr "მდგომარეობა შეიცვალა" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "გადასვლა შემდეგ დროის ბლოკზე, ან როცა შესვენების ჭდე შეიცვლება." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "გადაგეგმილია" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "ირთვება, როცა დაგეგმილი დროის ბლოკები იცვლება." #: src/core/event.vala:374 msgid "Expired" msgstr "ვადაგასულია" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "ირთვება, როცა ახლოვდება დრო, როცა მოხდება სესიის ჩამოყრა უქმეობის გამო." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "შესვენება" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "მოკლე შესვენება" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "გრძელი შესვენება" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro მალე დასრულდება" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "დასვენება" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "შესვენება მალე დასრულდება" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 წუთი" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "მოემზადეთ…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Pomodoro დასრულდა!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "შესვენება დასრულდა!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "დაადასტურეთ Pomodoro-ის გაშვება…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "დაადასტურეთ შესვენების დაწყება…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "დაადასტურეთ მოკლე შესვენების დაწყება…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "დაადასტურეთ გრძელი შესვენების დაწყება…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "შესვენების გამოტოვება" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "დაკვრის ინიციალიზაცია ჩავარდა" #: src/core/sounds.vala:112 msgid "File not found" msgstr "ფაილი ვერ მოიძებნა" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "ფაილის ტიპი მხარდაჭერილი არაა" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "გაჩერებულია" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "შესვენება" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "მოკლე შესვენება" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "დიდი შესვენება" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%uსთ" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%uწთ" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u საათი" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u წუთი" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u წამი" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "მიმდინარე მოვლენის ზუსტი დრო." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Pomodoro-ის ციკლის მიმდინარე ფაზა. შესაძლო მნიშვნელობებია: გაჩერებულია, pomodoro, შესვენება, მოკლე-შესვენება, " "გრძელი შესვენება." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "მიმდინარე დროის ბლოკის სტატუსი. შესაძლო მნიშვნელობებია: დაგეგმილია, " "მიმდინარეობს, დასრულდა, დაუსრულებელია." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "ალამი, რომელიც მიუთითებს, დაიწყო ათვლა, თუ არა." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "ალამი, რომელიც მიუთითებს, შეჩერებულია თუ არა ათვლა." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "ალამი, რომელიც მიუთითებს, დასრულდა ათვლა, თუ არა." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "ალამი, რომელიც მიუთითებს, ითვლის თუ არა ტაიმერი." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "მიმდინარე უკუათვლის ხანგრძლივობა." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "სხვაობა დადგენილსა და გასულ დროებს შორის." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "ათვლაზე დახარჯული დრო." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "ათვლის დასრულებამდე დარჩენილი დრო." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "ათვლის დაწყების დრო." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "GNOME Shell-ის გაფართოება" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "მიიღეთ საუკეთესო გამოცდილება!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "ჩართეთ GNOME Shell-ის გაფართოება გარემოსთან ინტეგრაციისთვის" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "ყოველთვის ხელმისაწვდომი" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "მართეთ ტაიმერი პირდაპირ ზედა პანელიდან აპლიკაციის გახსნის გარეშე" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "ნაკლები ყურადღების გაფანტვა" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "დახვეწილი შესვენების შეხსენებები" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "ელეგანტური სრული ეკრანის გალაგება შესვენებებს უფრო სასიამოვნოს ხდის" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "მზად ბრძანდებით, სცადოთ?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "გაფართოებ_ის დაყენება" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_არა ახლა" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "რაღაც ცუდადაა" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "ბუფერში კოპირება" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_თავიდან სცადეთ" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "_შეწყვეტა" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "მოლოდინის ვადა ამოიწურა" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "გაფართოებების დაყენება დაშვებული არაა" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "გაფართოების გადმოწერა ჩავარდა" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "ეკრანზე განლაგება" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "GNOME Shell-ის გაფართოება ხელმისაწვდომია" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "დაწვრილებით" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "გამოუყენებელი" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "დასრულდა!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "სტატისტიკა" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "გასვლა" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "ჟურნალი" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "ჟურნალის გასუფთავება" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "ჩანაწერები აქ გამოჩნდება ტაიმერის გაშვების შემდეგ." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "კონტექსტი" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "ბრძანება" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "გამოტანა" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "შეცდომა" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "გასვლის კოდი:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "შესრულების დრო:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "თემური დოღონაძე" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "შემოწირულობა" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "შესვენებები" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "წყვეტები" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "შესვენების თანაფარდობა" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "დღე" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "კვირა" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "თვე" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "ჯერჯერობით აქ არაფერია" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "დაასრულეთ რამდენიმე Pomodoros ამის შესავსებად!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "გამოტოვებულია %u დღე" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "გამოტოვებულია %u კვირა" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "გამოტოვებულია %u თვე" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "დღეს" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "გუშინ" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "ამ კვირაში" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "კვირა %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "კვირა %u %u-დან" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_მოკლე შესვენება" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_დიდი შესვენება" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_შესვენება" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "ეკრანზე განლაგების გახსნა" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "სესიის ვადა ამოიწურა" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "დიდი შესვენება დაიწყება %s-ში" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "გადახვევა ერთი წუთით" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_კომპაქტური ხედი" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_მორგება" #: src/ui/main/window.ui:19 msgid "_About" msgstr "შესახებ" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "გასვ_ლა" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "ძირითადი მენიუ" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "იყოს ტაიმერი გაშვებული?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "შეგიძლიათ დატოვოთ ფონურ რეჟიმში — შეტყობინებები და მალსახმობები მაინც " "იმუშავებს." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "გაშვება ფონურ რეჟიმში" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "დროა, შეისვენოთ" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "მთავარი ფანჯარა" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "მუქი თემის არჩევანი" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "კომპაქტური ხედის გამოყენება" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "გაშვებულია" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "შეჩერებულია" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "მორგებული ქმედების ჩასწორება" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "გაუ_ქმება" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_შენახვა" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "სახელი" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "ტრიგერი" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "მოვლენა" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "ბრძანების შესრულება მოვლენის შემდეგ." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "პირობა" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "მეორე ბრძანების შესრულება, როცა პირობა აღარ სრულდება." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "მოვლენები" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "_მოვლენის დამატება" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_ფილტრი" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "ფილტრი" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "გარსის ბრძანება" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "ბრძანებები" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "ბრძანება პირობის შესრულებისას" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "ბრძანება პირობის შეუსრულებლობისას" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "სამუშაო საქაღალდე" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "ქვეგარსის გამოყენება" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "პროგრამის გაშვება ქვეგარსიდან (მაგ. sh -c '')" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "შესატანი მონაცემების გადაცემა" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "ცვლადების ნაცვლად შეგიძლიათ JSON ობიექტის დამუშავება." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "დასრულების მოლოდინი" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "სხვა ბრძანებების დაბლოკვა მიმდინარე ბრძანების დასრულებამდე." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "ქმედების _წაშლა" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "მოვლენები მითითებული ჯერ არაა." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "მორგებული ქმედების დამატება" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_დამატება" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "აირჩიეთ სამუშაო საქაღალდე" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_აირჩიეთ" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "უსახელო ქმედება" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "პირობის დამატება" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "ჯგუფის დამატება" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "და" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ან" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "არის" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "არ არის" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "უდრის" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "მეტი, ვიდრე" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "ნაკლები, ვიდრე" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "დიახ" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "არა" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "წუთი" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "წამი" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "საათი" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "აირჩიეთ ველი…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "მდგომარეობა" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "გაშვებულია" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "ხანგრძლივობა" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "ცვლადის ჩასმა" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "ფორმატი" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "ჟურნა_ლი" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "შესრულების ჟურნალის ჩვენება" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "გარსის ბრძანებების ავტომატური გაშვება ტაიმერის მოვლენებზე. გაიგეთ მეტი." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "მალსახმობის დაყენება" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_დაყენება" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "გამორთულია" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "დააჭირეთ ღილაკს Esc გასაუქმებლად ან Backspace კლავიატურის " "მალსახმობის გამოსართავად" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "გლობალური მალსახმობებით აპლიკაციის მართვა შესაძლებელია მაშინაც კი, როცა ის " "ეკრანზე არ ჩანს. ისინი მუშაობენ მანამდე, სანამ აპი ფონურადაა გაშვებული." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "აპლიკაციის პარამეტრების გახსნა გლობალური მალსახმობების ჩასასწორებლად" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_ჩასწორება" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის გაშვების ან გაჩერებისთვის" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის გაშვების/შეჩერების/გაგრძელებისთვის" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის გასაშვებად" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის გასაჩერებლად" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის შესაჩერებლად" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "შეიყვანეთ ახალი მალსახმობი ტაიმერის გაგრძელებისთვის" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "შეიყვანეთ ახალი მალსახმობი გამოტოვებისთვის" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "გადახვევა ერთი წუთით" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "შეიყვანეთ ახალი მალსახმობი გადახვევისთვის" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "შეიყვანეთ ახალი მალსახმობი ფანჯრის ფოკუსირებისთვის" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "განცხადებები" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "დრო იწურება" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "შეტყობინება, როცა Pomodoro ან შესვენება სრულდება." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "სრულეკრანის შეტყობინება შესვენების აღების დასაძალებლად." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "დაბლოკვის დაყოვნება" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "უმოქმედობის პერიოდი ეკრანის დასაბლოკად." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "ხელახლა გახსნის დაყოვნება" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "უმოქმედობის პერიოდი გადაფარვის ხელახლა გასახსნელად." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "არასდროს" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "გაფრთხილებები" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "ხმები" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "გარეგნობა" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "კლავიატურის მალსახმობები" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "ავტომატიზაცია" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "ხმები გამორთულია" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "განგაშის ხმები" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Pomodoro-ის დასრულების ხმა" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "შესვენების დასრულების ხმა" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "ფონური ხმა" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "ზარი" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "ხმამაღალი ზარი" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "საათის წიკწიკი" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "არცერთი" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "ხმა:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "აირჩიეთ თქვენი ხმა" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Pomodoro-ის ხანგრძლივობა" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "მოკლე შესვენების ხანგრძლივობა" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "გრძელი შესვენების ხანგრძლივობა" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "ციკლების რაოდენობა" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "ქცევა" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "პაუზა ეკრანის დაბლოკვით" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "შესვენების დაწყების დადასტურება" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Pomodoro-ის გაშვების დასტური" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "ერთი სესია გასტანს %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "დროის %u%% დაეთმობა შესვენებებს." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "გადავატარო ცვლილებები მიმდინარე Pomodoro-ზე?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "გადავატარო ცვლილებები მიმდინარე შესვენებაზე?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "გადატარება" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "გვერდითი პანელი" #~ msgid "Time management utility" #~ msgstr "დროის მართვის პროგრამა" #~ msgid "pomodoro;timer;" #~ msgstr "pomodoro;timer;ტაიმერი;" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "შეინარჩუნეთ ფოკუსი ხშირი შესვენებების მეშვეობით" #~ msgid "Visual and audio notifications" #~ msgstr "ვიზუალური და ხმოვანი შეტყობინებები" #~ msgid "Time tracking and statistics" #~ msgstr "დროის თვალყურის დევნება და სტატისტიკა" #~ msgid "GNOME desktop integration" #~ msgstr "GNOME გარემოსთან ინტეგრაცია" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "მომხმარებლის ბრძანებების გაშვება Pomodoro-ის, ან შესვენების შემდეგ" #~ msgid "15 seconds" #~ msgstr "15 წამი" #~ msgid "30 seconds" #~ msgstr "30 წამი" #~ msgid "1 minute" #~ msgstr "1 წუთი" #~ msgid "2 minutes" #~ msgstr "2 წუთი" #~ msgid "3 minutes" #~ msgstr "3 წუთი" #~ msgid "5 minutes" #~ msgstr "5 წუთი" #~ msgid "Compact timer" #~ msgstr "კომპაქტური ტაიმერი" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "gnome-pomodoro 0.28.1-ის ცვლილებების მიმოხილვა" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "დაემატა ტამილური თარგმანი (მადლობა @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "დაემატა ებრაული თარგმანი (მადლობა @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "gnome-pomodoro 0.28.0-ის ცვლილებების მიმოხილვა" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "GNOME Shell 49-ის მხარდაჭერა (მადლობა @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "განახლდა გერმანული თარგმანი (მადლობა @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "gnome-pomodoro 0.27.0-ის ცვლილებების მიმოხილვა" #~ msgid "Support for GNOME Shell 48" #~ msgstr "GNOME Shell 48-ის მხარდაჭერა" #~ msgid "Split time spent across midnight" #~ msgstr "შუაღამისას დახარჯული დროის გაყოფა" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "დაემატა ტელუგური თარგმანი (მადლობა @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "gnome-pomodoro 0.26.0-ის ცვლილებების მიმოხილვა" #~ msgid "Support for GNOME Shell 47" #~ msgstr "GNOME Shell 47-ის მხარდაჭერა" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "ეკრანის განლაგების ჟესტით დახურვის ნებართვა ვიდეოს დაკვრისას" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "დაემატა ქართული თარგმანი (მადლობა @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "გასწორდა თარგმანები appdata-ში (მადლობა @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "gnome-pomodoro 0.25.2-ის ცვლილებების მიმოხილვა" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "შეტყობინების დარჩენის გასწორება Pomodoro გაგრძელების შემდეგ" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "gnome-pomodoro 0.25.1-ის ცვლილებების მიმოხილვა" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "შესწორებები GNOME Shell 46-ისთვის" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "GNOME Shell 45-ის მხარდაჭერის შეწყვეტა" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "gnome-pomodoro 0.25.0-ის ცვლილებების მიმოხილვა" #~ msgid "Support for GNOME Shell 46" #~ msgstr "GNOME Shell 46-ის მხარდაჭერა" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "აგების სკრიპტის მორგება meson 0.59.0-სთვის (მადლობა @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Pomodoro-ისთვის სისტემური გაფრთხილებების მართვის უფლების მიცემა, " #~ "სანამ ტაიმერი გაშვებულია" #~ msgid "Timer Ticking" #~ msgstr "ტაიმერის წიკწიკი" #~ msgid "Birds" #~ msgstr "ჩიტები" focustimerhq-FocusTimer-8581be2/po/kk.po000066400000000000000000002073361520625676500202550ustar00rootroot00000000000000# Kazakh translation for focus-timer # Copyright (c) 2017 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Baurzhan Muftakhidinov , 2017. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 14:18+0100\n" "Last-Translator: Baurzhan Muftakhidinov \n" "Language-Team: Kazakh\n" "Language: kk\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Бұл өнімділік таймері уақытыңызды зейінді жұмыс сеанстарына және қысқа " "үзілістерге бөлу арқылы тиімді жұмыс істеуге көмектеседі. Зейінді сақтау " "және шаршап қалмау үшін 25 минут жұмыс істеп, содан кейін 5 минут үзіліс " "жасаңыз." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Негізгі мүмкіндіктері:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Жұмыс және үзіліс ұзақтығын теңшеу" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Үзіліс кезіндегі экран қабаты" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Таймер" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Күндік статистика" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Айлық статистика" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Баптаулар" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Экран қабаты" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Бастау немесе Тоқтату" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Бастау, Кідірту немесе Жалғастыру" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Помодороны бастау" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Бастау" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Тоқтату" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Кідірту" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Жалғастыру" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Аттап кету" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Кері айналдыру" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Ағымдағы помодороны немесе үзілісті ұзарту" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Тастау" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Баптауларды көрсету" #: src/application.vala:203 msgid "Quit application" msgstr "Қолданбадан шығу" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Нұсқа ақпаратын шығару және шығу" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Назарға алу" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s қалды" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "\"%s\" таңдамалы әрекеті сәтсіз аяқталды" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Күту уақыты аяқталды" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Команданы орындау сәтсіз аяқталды" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Команда бос" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Жабылмаған тырнақша" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Қате команда" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Белгісіз айнымалы \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Белгісіз формат \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "\"%s\" бағдарламасы табылмады" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Әрекеттер" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Кері санақ" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Сеанс" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Басқа" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Таймер іске қосылды." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Таймер қолмен тоқтатылды." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Кері санақ қолмен кідіртілді. Экранды құлыптау немесе жүйені ұйқы режиміне " "өткізу кезінде іске қосылмайды." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Кері санақ қолмен жалғастырылды." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Кері санақ аяқталғанға дейін келесі уақыт блогына өтті." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Кері айналдыру әрекеті қолданылды. Ол өткен уақытқа кідіріс қосады." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Сеанс қолмен тазартылды." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Аяқталды" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Кері санақ аяқталды. Егер растау күтілсе, уақыт блогының ұзақтығын әлі де " "өзгертуге болады." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Өзгертілді" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Кері санаққа қатысты кез келген өзгерісте іске қосылады." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Келесіге өтуді растау" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Келесі уақыт блогын бастау үшін қолмен растау қажет." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Өтілді" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Келесі уақыт блогына ауысты немесе оны өткізіп жіберді." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Күй өзгерді" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Келесі уақыт блогына ауысқанда немесе үзіліс белгісі өзгергенде." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Қайта жоспарланды" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Жоспарланған уақыт блоктары өзгерген кезде іске қосылады." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Мерзімі бітті" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Әрекетсіздікке байланысты сеанс тасталу алдында іске қосылады." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Помодоро" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Үзіліс жасаңыз" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Қысқа үзіліс жасаңыз" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Ұзақ үзіліс жасаңыз" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Помодоро аяқталуға жақын" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Үзіліс жасаңыз" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Үзіліс аяқталуға жақын" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 минут" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Дайын болыңыз…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Помодоро аяқталды!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Үзіліс аяқталды!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Помодороның басталуын растаңыз…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Үзілістің басталуын растаңыз…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Қысқа үзілістің басталуын растаңыз…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Ұзақ үзілістің басталуын растаңыз…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Үзілісті аттап кету" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Ойнатуды іске қосу сәтсіз аяқталды" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Файл табылмады" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Файл түріне қолдау көрсетілмейді" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Тоқтатылды" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Үзіліс" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Қысқа үзіліс" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Ұзақ үзіліс" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u сағ" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u мин" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u сағат" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u минут" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u секунд" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Ағымдағы оқиғаның нақты уақыты." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Помодоро циклінің ағымдағы фазасы. Мүмкін мәндер: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Ағымдағы уақыт блогының күйі. Мүмкін мәндер: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Кері санақтың басталғанын көрсететін жалауша." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Кері санақтың кідіртілгенін көрсететін жалауша." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Кері санақтың аяқталғанын көрсететін жалауша." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Таймердің белсенді кері санап жатқанын көрсететін жалауша." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Ағымдағы кері санақтың ұзақтығы." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Өткен уақыт пен нақты уақыт арасындағы айырмашылық." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Кері санаққа жұмсалған уақыт мөлшері." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Кері санақ аяқталғанға дейін қалған уақыт мөлшері." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Кері санақ басталған уақыт." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell кеңейтімі" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Ең жақсы тәжірибені алыңыз!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Жұмыс үстелімен үздіксіз интеграция үшін GNOME Shell кеңейтімін " "қосыңыз" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Әрқашан қолжетімді" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Қолданбаны ашпай-ақ таймерді тікелей жоғарғы панельден басқарыңыз" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Зейінді бөлетін нәрселер аз" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Түзетілген үзіліс ескертулері" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Үзіліс жасауды жағымдырақ ететін талғампаз толық экранды қабат" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Байқап көруге дайынсыз ба?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Кеңейтімді орнату" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Қазір емес" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Бірдеңе дұрыс болмады" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Алмасу буферіне көшіру" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Қайтадан байқап көру" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Тоқтату" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Күту уақыты аяқталды" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Кеңейтімдерді орнатуға рұқсат етілмейді" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Кеңейтімді жүктеп алу сәтсіз аяқталды" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "Экран қабаты" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Жұмыс үстелі" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell кеңейтімі қолжетімді" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Көбірек білу" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Қолданылмаған" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Аяқталды!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Статистика" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Шығу" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Журнал" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Бос журнал" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Таймерді іске қосқан кезде жазбалар осында пайда болады." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Контекст" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Команда" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Шығыс" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Қате" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Шығу коды:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Орындалу уақыты:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Baurzhan Muftakhidinov " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Донат жасау" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Үзілістер" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Үзілістер (кедергілер)" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Үзіліс коэффициенті" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Күн" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Апта" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Ай" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Әзірге көретін ештеңе жоқ" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Бұл жерді толтыру үшін бірнеше помодороны аяқтаңыз!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u күн аттап өтілді" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u апта аттап өтілді" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u ай аттап өтілді" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Бүгін" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Кеше" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Осы аптада" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "%u-апта" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "%u-дан %u-апта" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Помодоро" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Қы_сқа үзіліс" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Ұ_зақ үзіліс" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Үзіліс" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Экран қабатын ашу" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Сеанс мерзімі аяқталды" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Ұзақ үзіліске дейін %s қалды" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Бір минутқа кері айналдыру" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Шағын көрініс" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Баптаулар" #: src/ui/main/window.ui:19 msgid "_About" msgstr "Осы тур_алы" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Шығу" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Негізгі мәзір" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "Таймер жұмысын жалғастыра берсін бе?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Сіз оны фондық режимде қалдыра аласыз — хабарламалар мен пернетақта " "жарлықтары әлі де жұмыс істейтін болады." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Фонда орындау" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Үзіліс жасайтын уақыт келді" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Негізгі терезе" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Қараңғы тақырыпты таңдау" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Шағын көріністі таңдау" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Басталды" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "Кідіртілді" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Таңдамалы әрекетті өңдеу" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Бас тарту" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Сақтау" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Аты" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Триггер" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Оқиға" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Оқиғадан кейін команданы орындау." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Шарт" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Шарт орындалмаған жағдайда екінші команданың орындалуын қамтамасыз ету." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Оқиғалар" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "_Оқиға қосу" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Сүзгі" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Сүзгі" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell командасы" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Командалар" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Шарт орындалғандағы команда" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Шарт орындалмағандағы команда" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Жұмыс каталогы" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Subshell қолдану" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Бағдарламаны sh -c '' сияқты subshell арқылы іске қосу" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Кіріс деректерін беру" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "Айнымалыларды берудің орнына JSON нысанын өңдеуге болады." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Аяқталуын күту" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Команда аяқталғанша басқа командалардың орындалуын блоктау." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "Әрекетті _өшіру" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Әзірге оқиғалар көрсетілмеген." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Таңдамалы әрекет қосу" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Қосу" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Жұмыс каталогын таңдау" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Таңдау" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Атаусыз әрекет" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Шарт қосу" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Топ қосу" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 #, fuzzy msgid "AND" msgstr "ЖӘНЕ" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 #, fuzzy msgid "OR" msgstr "НЕМЕСЕ" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Болса" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Болмаса" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Тең" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Үлкен" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Кіші" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Иә" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Жоқ" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Минут" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Секунд" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Сағат" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Өрісті таңдау…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Күйі" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Орындалуда" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Ұзақтығы" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Айнымалыны енгізу" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Формат" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Журнал" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Орындалу журналын көрсету" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Таймер оқиғаларында немесе шарттарында shell командаларын автоматты түрде " "орындау. Көбірек білу." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Жарлықты орнату" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Орнату" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Сөндірулі" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Бас тарту үшін Esc, ал жарлықты сөндіру үшін Backspace басыңыз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Глобалды жарлықтар қолданба экранда болмаса да оны басқаруға мүмкіндік " "береді. Олар қолданба фонда жұмыс істеп тұрғанда істей береді." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Глобалды жарлықтарды өңдеу үшін баптауларды ашу" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Өңдеу" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Таймерді бастау немесе тоқтату үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Таймерді бастау/кідірту/жалғастыру үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Таймерді бастау үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Таймерді тоқтату үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Таймерді кідірту үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Таймерді жалғастыру үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Аттап кету үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Бір минутқа кері айналдыру" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Кері айналдыру үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Терезені назарға алу үшін жаңа жарлықты енгізіңіз" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Хабарландырулар" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Уақыт таусылуда" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Помодоро немесе үзіліс аяқталуға жақын болғанда хабарлау." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Үзіліс жасауға мәжбүрлейтін толық экранды хабарлама." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Құлыптау кідірісі" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Экранды құлыптау үшін қажет әрекетсіздік кезеңі." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Қайта ашу кідірісі" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Жабылғаннан кейін қабатты қайта ашу үшін қажет әрекетсіздік кезеңі." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Ешқашан" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Хабарламалар" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Дыбыстар" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Сыртқы түрі" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Пернетақта жарлықтары" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Автоматизация" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Дыбыстар сөндірілген" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Ескерту дыбыстары" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Помодоро аяқталғандағы дыбыс" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Үзіліс аяқталғандағы дыбыс" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Фондық дыбыс" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Қоңырау" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Қатты қоңырау" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Сағат тықылы" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Жоқ" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Дыбыс деңгейі:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Таңдаулы дыбысты таңдау" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Помодоро ұзақтығы" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Қысқа үзіліс ұзақтығы" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Ұзақ үзіліс ұзақтығы" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Циклдер саны" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Мінез-құлық" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Экранды құлыптау арқылы кідірту" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Үзілістің басталуын растау" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Помодороның басталуын растау" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "Бір сеанс %s уақыт алады." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "Уақыттың %u%% үзілістерге бөлінеді." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Өзгерістерді ағымдағы помодороға қолдану керек пе?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Өзгерістерді ағымдағы үзіліске қолдану керек пе?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Қолдану" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Бүйірлік панель" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Уақытты басқару утилитасы" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Жиі үзіліс жасау арқылы зейінді сақтаңыз" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Визуалды және дыбыстық хабарламалар" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Уақытты бақылау және статистика" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "GNOME жұмыс үстелімен интеграция" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Помодоро немесе үзілістен кейін командаларды орындау" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Шағын таймер" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "gnome-pomodoro 0.28.1 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Тамил тіліндегі аударма қосылды (@omeritzics-ке рахмет)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Иврит тіліндегі аударма қосылды (@Killersparrow1-ге рахмет)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "gnome-pomodoro 0.28.0 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "GNOME Shell 49 қолдауы (@aleasto-ға рахмет)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Неміс тіліндегі аударма жаңартылды (@daPhipz-ке рахмет)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "gnome-pomodoro 0.27.0 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "GNOME Shell 48 қолдауы" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Түн ортасынан асқан уақытты бөлу" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Телугу тіліндегі аударма қосылды (@SpaciousCoder78-ге рахмет)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "gnome-pomodoro 0.26.0 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "GNOME Shell 47 қолдауы" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Бейне ойнап жатқанда экран қабатын ишарамен жабуға рұқсат беру" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Грузин тіліндегі аударма қосылды (@NorwayFun-ге рахмет)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Appdata аудармалары түзетілді (@yakushabb-ке рахмет)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "gnome-pomodoro 0.25.2 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Помодороны ұзартқаннан кейін хабарламаның қалып қоюы түзетілді" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "gnome-pomodoro 0.25.1 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "GNOME Shell 46 үшін түзетулер" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "GNOME Shell 45 қолдауын тоқтату" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "gnome-pomodoro 0.25.0 нұсқасындағы өзгерістер" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "GNOME Shell 46 қолдауы" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Құрастыру скрипті meson 0.59.0 нұсқасына бейімделді (@mattst88-ге рахмет)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Таймер жұмыс істеп тұрғанда Помодороға жүйелік хабарламаларды " #~ "басқаруға мүмкіндік беріңіз" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 секунд" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 секунд" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 минут" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 минут" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 минут" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 минут" #~ msgid "Timer Ticking" #~ msgstr "Таймер тықылы" #, fuzzy #~ msgid "Birds" #~ msgstr "Құстар" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;таймер;уақыт;" #~ msgid "Start/Stop" #~ msgstr "Бастау/Тоқтату" #~ msgid "Pause/Resume" #~ msgstr "Кідірту/Жалғастыру" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Помодороға немесе үзіліске өтіңіз" #~ msgid "Reset current session" #~ msgstr "Ағымдағы сеансты тастау" #~ msgid "Run as background service" #~ msgstr "Фон қызметі ретінде орындау" #~ msgid "About Pomodoro" #~ msgstr "Pomodoro туралы" #~ msgid "A simple time management utility" #~ msgstr "Уақытты басқарудың қарапайым утилитасы" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Тоқтату" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Кеңейтуді іске қосу сәтсіз аяқталды" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Ұзақ үзіліс ұзақтығы" #~ msgid "A time management utility for GNOME" #~ msgstr "GNOME үшін уақытты басқару утилитасы" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Помодоро техникасына сай уақытты басқаруға көмектесетін GNOME утилитасы. " #~ "Ол жұмыстың әр 25 минут сайын қысқа үзілістерді жасау арқылы өнімділікті " #~ "және назарды жақсартуға көмектесуге арналған." #~ msgid "Timer window" #~ msgstr "Таймер терезесі" #~ msgid "Indicator for GNOME Shell" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "GNOME Shell үшін индикатор" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "GNOME Shell үшін индикатор" #~ msgid "_Timer" #~ msgstr "_Таймер" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Таймерді іске қосу/сөндіру үшін пернетақта жарлығы. Өзгерту үшін жаңа " #~ "жарлықты енгізіңіз." #~ msgid "Pomodoros before a long break" #~ msgstr "Ұзақ үзіліске дейінгі помодоро саны" #~ msgid "Keyboard shortcut" #~ msgstr "Пернетақта жарлығы" #~ msgid "Screen notifications" #~ msgstr "Экрандағы хабарламалар" #~ msgid "Wait for activity after a break" #~ msgstr "Үзілістен кейін белсенділікті күту" #~ msgid "Plugins…" #~ msgstr "Плагиндер…" #~ msgid "Plugins" #~ msgstr "Плагиндер" #~ msgid "Back" #~ msgstr "Артқа" #~ msgid "Complete a few sessions" #~ msgstr "Бірнеше сеанстарды аяқтаңыз" #~ msgid "Previous (Alt+Left)" #~ msgstr "Алдыңғы (Alt+Сол)" #~ msgid "Next (Alt+Right)" #~ msgstr "Келесі (Alt+Оң)" #~ msgid "Complete" #~ msgstr "Аяқталды" #~ msgid "Enable" #~ msgstr "Іске қосу" #~ msgid "Add" #~ msgstr "Қосу" #~ msgid "Remove" #~ msgstr "Өшіру" #~ msgid "Elapsed Time" #~ msgstr "Өткен уақыты" #~ msgid "Pause Timer" #~ msgstr "Таймерді аялдату" #~ msgid "Pause break" #~ msgstr "Кідірту" #~ msgid "Pause Pomodoro" #~ msgstr "Кідірту" #~ msgid "Resume break" #~ msgstr "Жалғастыру" #~ msgid "Resume Pomodoro" #~ msgstr "Жалғастыру" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d минут қалды" #~ msgid "Report issue" #~ msgstr "Ақаулық жөнінде хабарлау" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "%s қызметін жөнелту сәтсіз аяқталды" #~ msgid "Woodland Birds" #~ msgstr "Құстар" #~ msgid "End of Break Sound" #~ msgstr "Үзілістің аяқталу дыбысы" #~ msgid "Start of Break Sound" #~ msgstr "Үзілістің басталу дыбысы" #~ msgid "Off" #~ msgstr "Сөнд." #~ msgid "Ticking sound" #~ msgstr "Тықылдайтын дыбыс" #~ msgid "Start of break sound" #~ msgstr "Үзілістің басталу дыбысы" #~ msgid "End of break sound" #~ msgstr "Үзілістің аяқталу дыбысы" #~ msgid "Focus on your task." #~ msgstr "Тапсырмаңызға бар күшіңізді салыңыз." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Сізде %d минут бар" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Сізде %d секунд бар" #~ msgid "Take a longer break" #~ msgstr "Ұзағырақ үзілісті алыңыз" #~ msgid "Lengthen it" #~ msgstr "Оны ұзарту" #~ msgid "Shorten it" #~ msgstr "Оны қысқарту" #~ msgid "Start pomodoro" #~ msgstr "Pomodoro іске қосу" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Жарлық ретінде \"%s\" қолдануды теруге теріс әсер тигізеді. Басқа пернені " #~ "қосып көріңіз, мысалы, Control, Alt немесе Shift." #~ msgid "Available" #~ msgstr "Бар" #~ msgid "Busy" #~ msgstr "Бос емес" #~ msgid "Idle" #~ msgstr "Іссіз" #~ msgid "Invisible" #~ msgstr "Жасырын" #, c-format #~ msgid "%d m" #~ msgstr "%d м" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f с" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f с" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "Remind to take a break" #~ msgstr "Үзілісті алу туралы еске салу" #, javascript-format #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d жаңа хабарлама" #~ msgid "Take a break!" #~ msgstr "Үзілісті алыңыз!" #, javascript-format #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Сізде келесі помодороға дейін %d минут бар." #, javascript-format #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Сізде келесі помодороға дейін %d секунд бар." #~ msgid "Hey!" #~ msgstr "Назар аударыңыз!" #~ msgid "You're missing out on a break" #~ msgstr "Үзілісті жіберіп отырсыз" #~ msgid "It seems to be uninstalled" #~ msgstr "Ол жойылған сияқты" #~ msgid "Extension is out of date" #~ msgstr "Кеңейту ескірген" #~ msgid "Upgrade" #~ msgstr "Жаңарту" #~ msgid "Remove Sound" #~ msgstr "Дыбысты өшіру" focustimerhq-FocusTimer-8581be2/po/ko.po000066400000000000000000001703501520625676500202540ustar00rootroot00000000000000# Korean translation for focus-timer # Copyright (c) 2020 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # root , 2020. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 14:25+0100\n" "Last-Translator: root \n" "Language-Team: Korean\n" "Language: ko\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "집중 업무 세션과 짧은 휴식으로 시간을 나누어 효율적으로 일할 수 있게 도와주" "는 생산성 타이머입니다. 25분간 업무를 수행한 후, 5분간 휴식을 취하여 집중력" "을 유지하고 번아웃을 방지하세요." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "주요 기능:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "사용자 설정 가능한 업무 세션 및 휴식 시간" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "휴식 시간 중 화면 오버레이" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "타이머" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "일일 통계" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "월간 통계" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "환경설정" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "화면 오버레이" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "시작 또는 정지" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "시작, 일시 정지 또는 재개" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "포모도로 시작하기" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "시작" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "정지" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "일시 정지" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "재개" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "건너뛰기" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "감기" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "현재 포모도로 또는 휴식 연장" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "초기화" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "설정 보기" #: src/application.vala:203 msgid "Quit application" msgstr "애플리케이션 종료" #: src/application.vala:206 msgid "Print version information and exit" msgstr "버전 정보 출력 및 종료" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "창 활성화" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s 남음" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "사용자 지정 동작 \"%s\" 실패" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "제한 시간 도달" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "명령 실행 실패" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "명령이 비어 있습니다" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "닫히지 않은 따옴표" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "유효하지 않은 명령" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "알 수 없는 변수 \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "알 수 없는 형식 \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "\"%s\" 프로그램을 찾을 수 없습니다" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "동작들" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "카운트다운" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "세션" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "기타" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "타이머를 시작했습니다." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "타이머를 수동으로 중지했습니다." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "카운트다운이 수동으로 일시 정지되었습니다. 화면 잠금 또는 시스템 대기 시에는 " "트리거되지 않습니다." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "카운트다운이 수동으로 재개되었습니다." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "카운트다운이 끝나기 전에 다음 시간 블록으로 이동했습니다." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "감기 동작이 사용되었습니다. 과거 기록에 일시 정지를 추가합니다." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "세션을 수동으로 지웠습니다." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "종료됨" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "카운트다운이 종료되었습니다. 확인을 기다리는 중이라면 시간 블록의 길이를 여전" "히 변경할 수 있습니다." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "변경됨" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "카운트다운과 관련된 변경 사항이 있을 때 트리거됩니다." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "진행 확인" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "다음 시간 블록을 시작하려면 수동 확인이 필요합니다." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "진행됨" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "다음 시간 블록으로 전환되거나 건너뛰었습니다." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "상태 변경됨" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "다음 시간 블록으로 전환되거나 휴식 라벨이 변경될 때 발생합니다." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "재일정됨" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "예정된 시간 블록이 변경되었을 때 트리거됩니다." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "만료됨" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "활동이 없어 세션이 초기화되기 직전에 트리거됩니다." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "포모도로" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "휴식을 취하세요" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "잠시 휴식" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "긴 휴식을 취하십시오" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "포모도로가 곧 끝납니다" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "휴식을 취하세요" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "휴식이 곧 끝납니다" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1분" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "준비하세요…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "포모도로가 끝났습니다!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "휴식이 끝났습니다!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "포모도로 시작 확인…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "휴식 시작 확인…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "짧은 휴식 시작 확인…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "긴 휴식 시작 확인…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "휴식 건너뛰기" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "재생 초기화 실패" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "파일을 찾을 수 없음" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "지원되지 않는 파일 형식" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "중지됨" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "휴식" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "짧은 휴식" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "긴 휴식" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u시간" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u분" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u시간" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u분" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u초" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "현재 이벤트의 정확한 시각입니다." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "포모도로 주기의 현재 단계입니다. 가능한 값: stopped, pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "현재 시간 블록의 상태입니다. 가능한 값: scheduled, in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "카운트다운이 시작되었는지 나타내는 플래그입니다." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "카운트다운이 일시 정지되었는지 나타내는 플래그입니다." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "카운트다운이 완료되었는지 나타내는 플래그입니다." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "타이머가 활발하게 카운트다운 중인지 나타내는 플래그입니다." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "현재 카운트다운의 지속 시간입니다." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "경과된 시간과 실제 흐른 시간 사이의 불일치입니다." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "카운트다운에 소요된 시간입니다." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "카운트다운이 끝나기까지 남은 시간입니다." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "카운트다운이 시작된 시각입니다." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "그놈 쉘 확장" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "최고의 경험을 누리세요!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "원활한 데스크톱 통합을 위해 그놈 쉘 확장을 활성화하세요" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "언제 어디서나 간편하게" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "앱을 열지 않고도 상단 바에서 직접 타이머를 제어하세요" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "집중력 향상" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "세련된 휴식 알림" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "우아한 전체 화면 오버레이로 더욱 쾌적하게 휴식을 취하세요" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "사용해 보시겠습니까?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "확장 설치(_I)" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "나중에(_N)" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "문제가 발생했습니다" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "클립보드에 복사" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "다시 시도(_T)" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "중단(_A)" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "시간 초과" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "확장 설치가 허용되지 않았습니다" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "확장 다운로드 실패" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "화면 오버레이" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "데스크톱" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "그놈 쉘 확장을 사용할 수 있습니다" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "더 알아보기" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "사용되지 않음" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "완료되었습니다!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "통계" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "종료" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "로그" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "로그가 비어 있음" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "타이머를 시작하면 여기에 항목이 표시됩니다." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "컨텍스트" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "명령" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "출력" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "오류" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "종료 코드:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "실행 시간:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Dayeon Lee " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "기부하기" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "휴식 횟수" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "방해 요소" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "휴식 비율" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "일" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "주" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "월" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "아직 표시할 데이터가 없습니다" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "포모도로를 몇 번 완료하여 통계를 채워보세요!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u일 건너뜀" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u주 건너뜀" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u개월 건너뜀" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "오늘" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "어제" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "이번 주" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "%u주" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "%u주 중 %u번째 주" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_포모도로" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_짧은 휴식" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_긴 휴식" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "휴식(_B)" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "화면 오버레이 열기" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "세션이 만료되었습니다" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "긴 휴식까지 %s 남음" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "1분 감기" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "간편 보기(_C)" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "환경설정(_P)" #: src/ui/main/window.ui:19 msgid "_About" msgstr "정보(_A)" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "종료(_Q)" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "기본 메뉴" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "타이머를 계속 실행할까요?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "배경에서 계속 실행할 수 있습니다. 알림과 키보드 단축키는 여전히 작동합니다." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "배경에서 실행" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "휴식을 취할 시간입니다" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "주 창" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "어두운 테마 선호" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "간편 보기 선호" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "시작됨" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "일시 정지됨" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "사용자 지정 동작 편집" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "취소(_C)" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "저장(_S)" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "이름" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "트리거" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "이벤트" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "이벤트 발생 후 명령을 실행합니다." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "조건" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "조건이 더 이상 충족되지 않을 때 두 번째 명령이 실행되도록 합니다." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "이벤트" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "이벤트 추가(_E)" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "필터링(_F)" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "필터" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "쉘 명령" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "명령어" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "조건 충족 시 명령" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "조건 불충족 시 명령" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "작업 디렉터리" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "서브쉘 사용" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "sh -c ''와 같은 서브쉘에서 프로그램을 실행합니다" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "입력 데이터 전달" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "변수를 전달하는 대신 JSON 객체를 처리할 수 있습니다." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "완료까지 대기" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "명령이 완료될 때까지 다른 명령의 실행을 차단합니다." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "동작 삭제(_D)" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "지정된 이벤트가 없습니다." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "사용자 지정 동작 추가" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "추가(_A)" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "작업 디렉터리 선택" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "선택(_S)" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "제목 없는 동작" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "조건 추가" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "그룹 추가" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "그리고 (AND)" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "또는 (OR)" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "~이다" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "~이 아니다" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "같음" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "~보다 큼" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "~보다 작음" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "예" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "아니요" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "분" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "초" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "시간" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "필드 선택…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "상태" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "실행 중" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "지속 시간" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "변수 삽입" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "형식" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "로그(_L)" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "실행 로그 보기" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "타이머 이벤트 또는 조건에 따라 쉘 명령을 자동으로 실행합니다. " "더 알아보기." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "단축키 설정" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "설정(_S)" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "비활성화됨" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "단축키를 취소하려면 Esc를, 비활성화하려면 Backspace를 누르세요" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "전역 단축키를 사용하면 앱이 화면에 없을 때도 제어할 수 있습니다. 앱이 배경에" "서 실행 중인 동안 작동합니다." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "전역 단축키 편집을 위해 앱 설정 열기" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "편집(_E)" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "타이머 시작 또는 중지를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "타이머 시작/일시 정지/재개를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "타이머 시작을 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "타이머 중지를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "타이머 일시 정지를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "타이머 재개를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "건너뛰기를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "1분 감기" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "감기를 위한 새 단축키 입력" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "창을 활성화하기 위한 새 단축키 입력" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "공지 및 알림" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "시간 종료 임박" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "포모도로 또는 휴식이 끝나기 직전에 알립니다." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "강제적으로 휴식을 취하도록 하는 전체 화면 알림입니다." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "잠금 지연" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "화면을 잠그기 전까지의 비활동 시간입니다." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "재개방 지연" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "오버레이를 닫은 후 다시 나타나기 전까지의 비활동 시간입니다." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "안 함" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "알림" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "소리" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "모양" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "키보드 단축키" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "자동화" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "소리가 비활성화됨" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "알림음" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "포모도로 종료 시 소리" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "휴식 종료 시 소리" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "배경음" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "벨" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "큰 벨 소리" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "시계 똑딱 소리" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "없음" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "음량:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "사용자 지정 소리 선택" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "포모도로 시간" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "짧은 휴식 시간" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "긴 휴식 시간" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "주기 횟수" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "동작" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "화면 잠금 시 일시 정지" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "휴식 시작 전 확인" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "포모도로 시작 전 확인" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "단일 세션 소요 시간: %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "전체 시간의 %u%%가 휴식에 할당됩니다." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "진행 중인 포모도로에 변경 사항을 적용할까요?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "진행 중인 휴식에 변경 사항을 적용할까요?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "적용" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "사이드바" #, fuzzy #~ msgid "Time management utility" #~ msgstr "시간 관리 유틸리티" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "주기적인 휴식을 통해 집중력을 유지하세요" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "시각 및 음성 알림" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "시간 추적 및 통계" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "그놈 데스크톱 통합" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "포모도로 또는 휴식 후 사용자 지정 명령 실행" #, fuzzy #~ msgid "Compact timer" #~ msgstr "간편 타이머" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "그놈 포모도로 0.28.1 변경 사항 개요" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "타밀어 번역 추가 (도움 주신 분: @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "히브리어 번역 추가 (도움 주신 분: @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "그놈 포모도로 0.28.0 변경 사항 개요" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "그놈 쉘 49 지원 (도움 주신 분: @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "독일어 번역 업데이트 (도움 주신 분: @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "그놈 포모도로 0.27.0 변경 사항 개요" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "그놈 쉘 48 지원" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "자정을 걸친 시간 기록 분할" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "텔루구어 번역 추가 (도움 주신 분: @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "그놈 포모도로 0.26.0 변경 사항 개요" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "그놈 쉘 47 지원" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "동영상 재생 중 제스처를 통한 화면 오버레이 닫기 허용" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "조지아어 번역 추가 (도움 주신 분: @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "appdata 내 번역 수정 (도움 주신 분: @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "그놈 포모도로 0.25.2 변경 사항 개요" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "포모도로 연장 후 알림이 유지되던 현상 수정" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "그놈 포모도로 0.25.1 변경 사항 개요" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "그놈 쉘 46 수정 사항" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "그놈 쉘 45 지원 중단" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "그놈 포모도로 0.25.0 변경 사항 개요" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "그놈 쉘 46 지원" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "빌드 스크립트를 meson 0.59.0에 맞게 조정 (도움 주신 분: @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "타이머가 실행되는 동안 포모도로가 시스템 알림을 관리하게 하세요" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15초" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30초" #, fuzzy #~ msgid "1 minute" #~ msgstr "1분" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2분" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3분" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5분" #~ msgid "Timer Ticking" #~ msgstr "타이머 똑딱 소리" #, fuzzy #~ msgid "Birds" #~ msgstr "새소리" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "타이머;timer;포모도로;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "시작/정지" #~ msgid "Pause/Resume" #~ msgstr "일시 정지/재개" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "포모도로 또는 휴식으로 건너뛰기" #~ msgid "Reset current session" #~ msgstr "현재 세션 초기화" #~ msgid "Run as background service" #~ msgstr "백그라운드 서비스로 실행" #~ msgid "About Pomodoro" #~ msgstr "포모도로 정보" #~ msgid "A simple time management utility" #~ msgstr "간단한 시간 관리 유틸리티" #, fuzzy #~ msgid "_Stopped" #~ msgstr "중지됨(_S)" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "그놈 쉘 확장을 사용할 수 있습니다" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "확장 설치 실패" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "긴 휴식까지 %s 남음" #~ msgid "A time management utility for GNOME" #~ msgstr "그놈용 시간 관리 유틸리티" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "포모도로 기법에 따라 시간 관리를 도와주는 그놈 유틸리티입니다. 25분 업무 " #~ "후 짧은 휴식을 취하여 생산성과 집중력을 높이는 것을 목표로 합니다." #~ msgid "Timer window" #~ msgstr "타이머 창" #~ msgid "Indicator for GNOME Shell" #~ msgstr "그놈 쉘 표시기" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "그놈 쉘 42 지원 (@milotype 및 @kappa)" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "그놈 쉘 41 지원 (@mbooth101)" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "그놈 쉘 40.0 지원 (4.0 아님)" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "그놈 쉘 4.0 지원" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "그놈 쉘 3.38 지원 (@ignapk 및 @szpak)" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "그놈 쉘 3.36 지원" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "그놈 쉘 3.34 전용 지원" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "그놈 쉘 3.32 지원 (@demokritos)" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "그놈 쉘 3.28 및 3.30 지원 (@aerostitch)" #~ msgid "_Timer" #~ msgstr "타이머(_T)" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "타이머 토글 단축키입니다. 변경하려면 새 단축키를 입력하세요." #~ msgid "Pomodoros before a long break" #~ msgstr "긴 휴식 전 포모도로 횟수" #~ msgid "Keyboard shortcut" #~ msgstr "키보드 단축키" #~ msgid "Screen notifications" #~ msgstr "화면 알림" #~ msgid "Wait for activity after a break" #~ msgstr "휴식 후 활동 대기" #~ msgid "Plugins…" #~ msgstr "플러그인…" #~ msgid "Plugins" #~ msgstr "플러그인" #~ msgid "Back" #~ msgstr "뒤로" #~ msgid "Complete a few sessions" #~ msgstr "세션을 몇 번 완료하세요" #~ msgid "Previous (Alt+Left)" #~ msgstr "이전 (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "다음 (Alt+Right)" #~ msgid "Complete" #~ msgstr "완료" #~ msgid "Enable" #~ msgstr "활성화" #~ msgid "Add" #~ msgstr "추가" #~ msgid "Remove" #~ msgstr "제거" #~ msgid "Elapsed Time" #~ msgstr "경과 시간" #~ msgid "Pause Timer" #~ msgstr "타이머 일시 정지" #~ msgid "Pause break" #~ msgstr "휴식 일시 정지" #~ msgid "Pause Pomodoro" #~ msgstr "포모도로 일시 정지" #~ msgid "Resume break" #~ msgstr "휴식 재개" #~ msgid "Resume Pomodoro" #~ msgstr "포모도로 재개" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d분 남음" #~ msgid "Report issue" #~ msgstr "문제 보고" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "%s 서비스 실행 실패" #~ msgid "Woodland Birds" #~ msgstr "숲속 새소리" #~ msgid "End of Break Sound" #~ msgstr "휴식 종료 알림음" #~ msgid "Start of Break Sound" #~ msgstr "휴식 시작 알림음" #~ msgid "Off" #~ msgstr "끔" #~ msgid "Ticking sound" #~ msgstr "똑딱 소리" #~ msgid "Start of break sound" #~ msgstr "휴식 시작 알림음" #~ msgid "End of break sound" #~ msgstr "휴식 종료 알림음" #~ msgid "Focus on your task." #~ msgstr "업무에 집중하세요." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "%d분이 남았습니다" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "%d초가 남았습니다" #~ msgid "Take a longer break" #~ msgstr "더 긴 휴식을 취하세요" #~ msgid "Lengthen it" #~ msgstr "늘리기" #~ msgid "Shorten it" #~ msgstr "줄이기" #~ msgid "Start pomodoro" #~ msgstr "포모도로 시작" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "\"%s\"을(를) 단축키로 사용하면 타이핑에 방해가 될 수 있습니다. Control, " #~ "Alt, Shift와 같은 다른 키를 조합해 보세요." #~ msgid "Available" #~ msgstr "대화 가능" #~ msgid "Busy" #~ msgstr "다른 용무 중" #~ msgid "Idle" #~ msgstr "자리 비움" #~ msgid "Invisible" #~ msgstr "오프라인으로 표시" #, c-format #~ msgid "%d m" #~ msgstr "%d분" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f시간" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f시간" #~ msgid "gnome-pomodoro" #~ msgstr "그놈 포모도로" #~ msgid "_Stats" #~ msgstr "통계(_S)" #~ msgid "It seems to be uninstalled" #~ msgstr "제거된 것 같습니다" #~ msgid "Extension is out of date" #~ msgstr "확장이 오래되었습니다" #~ msgid "Upgrade" #~ msgstr "업그레이드" focustimerhq-FocusTimer-8581be2/po/lt.po000066400000000000000000001664211520625676500202660ustar00rootroot00000000000000# Lithuanian translation for focus-timer # Copyright (c) 2016 Free Software Foundation, Inc. # This file is distributed under the same license as the focus-timer package. # # Authors: # Paulius Šukys , 2016. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 09:26+0200\n" "PO-Revision-Date: 2026-02-02 18:07+0200\n" "Last-Translator: Paulius Šukys \n" "Language-Team: Lithuanian\n" "Language: lt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "(n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 3.8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Produktyvumo laikmatis, padedantis dirbti efektyviau, suskirstant laiką į " "susikaupimo sesijas ir trumpas pertraukas. Dirbkite 25 minutes, tada " "darykite 5 minučių pertrauką, kad išlaikytumėte koncentraciją ir " "išvengtumėte perdegimo." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Pagrindinės savybės:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Pritaikoma darbo sesijų ir pertraukų trukmė" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Ekrano užsklanda pertraukų metu" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Laikmatis" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Dienos statistika" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Mėnesio statistika" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Nustatymai" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Ekrano užsklanda" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "Pradėti arba sustabdyti" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "Pradėti, pristabdyti arba tęsti" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Pradėti Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "Pradėti pertrauką" #: src/application.vala:167 msgid "Start short break" msgstr "Pradėti trumpą pertrauką" #: src/application.vala:170 msgid "Start long break" msgstr "Pradėti ilgą pertrauką" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Pradėti" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stabdyti" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pauzė" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Tęsti" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Praleisti" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Atsukti" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "SEKUNDĖS" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Pratęskite dabartinį pomodoro arba pertrauką" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Atstatyti" #: src/application.vala:197 msgid "Print timer status" msgstr "Atspausdinti laikmačio būseną" #: src/application.vala:200 msgid "Show preferences" msgstr "Rodyti nustatymus" #: src/application.vala:203 msgid "Quit application" msgstr "Išeiti iš programos" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Spausdinti versijos informaciją ir išeiti" #: src/application.vala:240 msgid "Timer Options:" msgstr "Laikmačio Pasirinkimai" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "Rodyti laikmačio kontrolės nustatymus" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "Pranešti apie defektus: %s" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Iškelti į priekį" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "Liko %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" "Negalima konfigūracija. Pateikite po vieną žymą vienu metu laikmačio " "kontrolei." #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "Pasirinktinis veiksmas „%s“ nepavyko" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Baigėsi laikas" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "Nepavyko įvykdyti komandos" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "Komanda yra tuščia" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Neuždaryta kabutė" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Neteisinga komanda" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Nežinomas kintamasis „%s“" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Nežinomas formatas „%s“" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Programa „%s“ nerasta" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Veiksmai" #: src/core/event.vala:183 msgid "Countdown" msgstr "Atgalinis skaičiavimas" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Sesija" #: src/core/event.vala:189 msgid "Other" msgstr "Kita" #: src/core/event.vala:269 msgid "Started the timer." msgstr "Laikmatis paleistas." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "Laikmatis sustabdytas rankiniu būdu." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Skaičiavimas buvo pristabdytas rankiniu būdu. Nepaleidžiama užrakinant " "ekraną ar užmigdant sistemą." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Skaičiavimas buvo pratęstas rankiniu būdu." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "Pereita prie kito laiko bloko nepasibaigus skaičiavimui." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "Panaudotas atsukimo veiksmas. Jis prideda pauzę praeityje." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Sesija išvalyta rankiniu būdu." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Baigta" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Skaičiavimas baigėsi. Jei laukiama patvirtinimo, laiko bloko trukmė vis dar " "gali būti pakeista." #: src/core/event.vala:333 msgid "Changed" msgstr "Pasikeitė" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "" "Aktyvuojama įvykus bet kokiam su atgaliniu skaičiavimu susijusiam pokyčiui." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Patvirtinti eigą" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "Norint pradėti kitą laiko bloką, reikalingas rankinis patvirtinimas." #: src/core/event.vala:350 msgid "Advanced" msgstr "Pažengta" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Pereita arba praleista į kitą laiko bloką." #: src/core/event.vala:358 msgid "State Changed" msgstr "Būsena pasikeitė" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Pereita į kitą laiko bloką arba kai pertrauka pervardijama." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Perplanuota" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Aktyvuojama pasikeitus suplanuotiems laiko blokams." #: src/core/event.vala:374 msgid "Expired" msgstr "Baigėsi laikas" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "Aktyvuojama, kai sesija turi būti atstatyta dėl neaktyvumo." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Daryti pertrauką" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Padarykite trumpą pertraukėlę" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Padarykite ilgą pertrauką" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro tuoj baigsis" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "Metas pertraukai" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Pertrauka tuoj baigsis" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 minutė" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Pasiruošk…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Pomodoro sesija baigėsi!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "Pertrauka baigėsi!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Patvirtinkite Pomodoro pradžią…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Patvirtinkite pertraukos pradžią…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Patvirtinkite trumpos pertraukos pradžią…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Patvirtinkite ilgos pertraukos pradžią…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Praleisti pertrauką" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Nepavyko pradėti atkūrimo" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Failas nerastas" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Failo tipas nepalaikomas" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "Sustabdyta" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pertrauka" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Trumpa pertrauka" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Ilga pertrauka" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%u val." #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%u min." #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u valanda" msgstr[1] "%u valandos" msgstr[2] "%u valandų" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minutė" msgstr[1] "%u minutės" msgstr[2] "%u minučių" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekundė" msgstr[1] "%u sekundės" msgstr[2] "%u sekundžių" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Tikslus dabartinio įvykio laikas." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Dabartinė Pomodoro ciklo fazė. Galimos reikšmės: sustabdyta, " "pomodoro, pertrauka, trumpa petrauka, ilga " "pertrauka." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Dabartinio laiko bloko būsena. Galimos reikšmės: suplanuota, " "vykdoma, baigta, nebaigta." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Žyma, nurodanti atgalinio skaičiavimo pradžią." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Žyma, nurodanti atgalinio skaičiavimo pristabdymą." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Žyma, nurodanti atgalinio skaičiavimo pabaigą." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Žyma, nurodanti, ar laikmatis aktyviai skaičiuoja laiką atgal." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Dabartinio atgalinio skaičiavimo trukmė." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Skirtumas tarp rodomo laiko trukmės ir faktinio laiko." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "Laikas, praleistas skaičiuojant atgal." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "Likęs laikas iki skaičiavimo atgal pabaigos." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Laikas, kada prasidėjo atgalinis skaičiavimas." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "GNOME Shell plėtinys" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Gaukite geriausią patirtį!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Įjunkite GNOME Shell plėtinį sklandžiai integracijai su aplinka" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Visada pasiekiama" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "" "Valdykite laikmatį tiesiai iš viršutinės juostos neatidarydami programos" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Mažiau trukdžių" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Patobulinti priminimai apie pertraukas" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Elegantiška viso ekrano užsklanda, padaranti pertraukas malonesnes" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Pasiruošę išbandyti?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "Į_diegti plėtinį" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_Ne dabar" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Kažkas nepavyko" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Kopijuoti į iškarpinę" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Bandykite dar kartą" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "_Atšaukti" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Baigėsi laikas" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Plėtinių diegimas neleidžiamas" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "Nepavyko atsisiųsti plėtinio" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Ekrano užsklanda" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Darbalaukis" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "Yra GNOME Shell plėtinys" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Sužinoti daugiau" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "Nenaudojama" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Baigta!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistika" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Išeiti" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Žurnalas" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Žurnalas tuščias" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Įrašai atsiras čia, kai paleisite laikmatį." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Kontekstas" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Komanda" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Išvestis" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Klaida" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Išėjimo kodas:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Vykdymo laikas:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Paulius Šukys " #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Paremkite" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "Pertraukos" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Pertraukimai" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "Pertraukų santykis" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Diena" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Savaitė" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mėnuo" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "Dar nėra ką pamatyti" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Užbaikite keletą Pomodoro sesijų, kad čia atsirastų duomenų!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Praleista %u diena" msgstr[1] "Praleista %u dienos" msgstr[2] "Praleista %u dienų" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Praleista %u savaitė" msgstr[1] "Praleista %u savaitės" msgstr[2] "Praleista %u savaičių" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Praleistas %u mėnuo" msgstr[1] "Praleisti %u mėnesiai" msgstr[2] "Praleista %u mėnesių" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Šiandien" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "vakar" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Šią savaitę" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "%u savaitė" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "%u savaitė iš %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Trumpa pertrauka" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Ilga pertrauka" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_Pertrauka" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Atidaryti ekrano užsklandą" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Sesija nebegalioja" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "Ilga pertrauka už %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "Atsukti vieną minutę" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_Kompaktiškas vaizdas" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Nustatymai" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Apie" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Išeiti" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Pagrindinis meniu" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Palikti laikmatį veikiantį?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Galite palikti jį veikti fone — pranešimai ir spartieji klavišai vis tiek " "veiks." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "Veikti fone" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Laikas padaryti pertrauką" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "Pagrindinis langas" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Naudoti tamsią temą" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Naudoti kompaktišką vaizdą" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "Pradėta" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Pristabdyta" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "Redaguoti pasirinktinį veiksmą" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Atsisakyti" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "Iš_saugoti" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Vardas" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "Trigeris" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Įvykis" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Vykdyti komandą po įvykio." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Sąlyga" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "Užtikrinti antros komandos vykdymą, kai sąlyga nebetenkinama." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Įvykiai" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Pridėti į_vykį" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filtras" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filtras" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "Shell komanda" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "Komandos" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Komanda, kai sąlyga tenkinama" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Komanda, kai sąlyga netenkinama" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Darbinis katalogas" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Naudoti Subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Vykdyti programą iš subshell, pvz., sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Perduoti įvesties duomenis" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "Vietoj kintamųjų perdavimo galite apdoroti JSON objektą." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Laukti pabaigos" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Blokuoti kitų komandų vykdymą, kol ši komanda bus baigta." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "Iš_trinti veiksmą" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Įvykių dar nenurodyta." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "Pridėti pasirinktinį veiksmą" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Pridėti" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Pasirinkti darbinį katalogą" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Pasirinkti" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "Bevardis veiksmas" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Pridėti sąlygą" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Pridėti grupę" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "IR" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ARBA" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Yra" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Nėra" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Lygu" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Daugiau nei" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Mažiau nei" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Taip" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Ne" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "Minutės" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Sekundės" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Valandos" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Pasirinkite lauką…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Būsena" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Vykdoma" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "Trukmė" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Įterpti kintamąjį" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Formatas" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "Ž_urnalas" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Rodyti vykdymo žurnalą" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Vykdykite shell komandas automatiškai pagal laikmačio įvykius ar sąlygas. Sužinokite daugiau." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "Nustatyti spartųjį klavišą" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "Nu_statyti" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Išjungti" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Spauskite Esc atšaukimui arba Backspace, jei norite išjungti " "spartųjį klavišą" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Visuotiniai spartieji klavišai leidžia valdyti programą net kai ji nėra " "ekrane. Jie veikia tol, kol programa veikia fone." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Atidaryti nustatymus visuotinių sparčiųjų klavišų redagavimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "R_edaguoti" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio paleidimui arba sustabdymui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio paleidimui/pauzei/tęsimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio paleidimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio sustabdymui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio pristabdymui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Įveskite naują spartųjį klavišą laikmačio pratęsimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Įveskite naują spartųjį klavišą praleidimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Atsukti vieną minutę" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Įveskite naują spartųjį klavišą atsukimui" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Įveskite naują spartųjį klavišą lango iškėlimui" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Pranešimai" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Laikas baigiasi" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "Pranešti, kai Pomodoro arba pertrauka tuoj baigsis." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Viso ekrano pranešimas, skirtas priversti padaryti pertrauką." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Užrakinimo delsa" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Neaktyvumo laikotarpis, po kurio užrakinamas ekranas." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Pakartotinio atidarymo delsa" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Neaktyvumo laikotarpis, po kurio vėl rodoma užsklanda ją išjungus." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Niekada" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Pranešimai" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Garsai" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Išvaizda" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "Spartieji klaviatūros klavišai" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "Automatizavimas" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Garsai išjungti" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "Įspėjimų garsai" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Pomodoro pabaigos garsas" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "Pertraukos pabaigos garsas" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "Fono garsas" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Varpelis" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Garsus varpas" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Laikrodžio tiksėjimas" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Nėra" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Garsumas:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Pasirinkti kitą garsą" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Pomodoro trukmė" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "Trumpos pertraukos trukmė" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "Ilgosios pertraukos trukmė" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Ciklų skaičius" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Elgsena" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Pristabdyti užrakinant ekraną" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Patvirtinti pertraukos pradžią" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Patvirtinti Pomodoro pradžią" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Vienos sesijos trukmė: %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% laiko bus skirta pertraukoms." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Pritaikyti pakeitimus vykstančiam Pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Pritaikyti pakeitimus vykstančiai pertraukai?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "Pritaikyti" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Šoninė juosta" #~ msgid "Time management utility" #~ msgstr "Laiko valdymo įrankis" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Išlaikykite susikaupimą darydami dažnas pertraukas" #~ msgid "Visual and audio notifications" #~ msgstr "Vaizdiniai ir garsiniai pranešimai" #~ msgid "Time tracking and statistics" #~ msgstr "Laiko sekimas ir statistika" #~ msgid "GNOME desktop integration" #~ msgstr "Integracija su GNOME aplinka" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Vykdykite pasirinktines komandas po Pomodoro sesijos ar pertraukos" #~ msgid "15 seconds" #~ msgstr "15 sekundžių" #~ msgid "30 seconds" #~ msgstr "30 sekundžių" #~ msgid "1 minute" #~ msgstr "1 minutė" #~ msgid "2 minutes" #~ msgstr "2 minutės" #~ msgid "3 minutes" #~ msgstr "3 minutės" #~ msgid "5 minutes" #~ msgstr "5 minutės" #~ msgid "Compact timer" #~ msgstr "Kompaktiškas laikmatis" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.28.1" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Pridėtas tamilų kalbos vertimas (ačiū @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Pridėtas hebrajų kalbos vertimas (ačiū @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.28.0" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Palaikymas GNOME Shell 49 (ačiū @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Atnaujintas vokiečių kalbos vertimas (ačiū @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.27.0" #~ msgid "Support for GNOME Shell 48" #~ msgstr "Palaikymas GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Padalinti laiką einantį per vidurnaktį" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Pridėtas telugų kalbos vertimas (ačiū @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.26.0" #~ msgid "Support for GNOME Shell 47" #~ msgstr "Palaikymas GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Leisti išjungti ekrano užsklandą gestu, kai rodomas vaizdo įrašas" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Pridėtas gruzinų kalbos vertimas (ačiū @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Pakoreguoti vertimai appdata faile (ačiū @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Ištaisyta klaida, kai pranešimas lieka pratęsus Pomodoro" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.25.1" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Pataisymai GNOME Shell 46 versijai" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Nebepalaikoma GNOME Shell 45" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Pakeitimų apžvalga gnome-pomodoro 0.25.0" #~ msgid "Support for GNOME Shell 46" #~ msgstr "Palaikymas GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Sukūrimo skriptas pritaikytas meson 0.59.0 (ačiū @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Leiskite Pomodoro valdyti sistemos pranešimus, kol veikia laikmatis" #~ msgid "Timer Ticking" #~ msgstr "Laikmačio tiksėjimas" #~ msgid "Birds" #~ msgstr "Paukščiai" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "laikmatis;laikrodis;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Pradėti/Stabdyti" #~ msgid "Pause/Resume" #~ msgstr "Pauzė/Tęsti" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Pereikite prie pomodoro arba į pertrauką" #~ msgid "Reset current session" #~ msgstr "Iš naujo nustatyti dabartinę sesiją" #~ msgid "Run as background service" #~ msgstr "Leisti kaip sistemos servisą" #~ msgid "About Pomodoro" #~ msgstr "Apie Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Paprastas laiko valdymo įrankis" #~ msgid "_Stopped" #~ msgstr "Stabdyti" #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Failed to install extension" #~ msgstr "Nepavyko įjungti įskiepio" #~ msgid "Long break due in %s" #~ msgstr "Ilgosios pertraukos trukmė" #~ msgid "A time management utility for GNOME" #~ msgstr "Laiko valdymo įrankis GNOME aplinkai" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "GNOME aplinkos įrankis, kuris padeda valdyti laiką pagal Pomodoro " #~ "metodiką. Jos paskirtis - patobulinti produktyvumą ir susikaupimą, darant " #~ "trumpas pertraukas tarp 25 koncentruoto darbo minučių." #~ msgid "Timer window" #~ msgstr "Laikmačio langas" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Indikatorius GNOME aplinkai" #~ msgid "_Timer" #~ msgstr "_Laikmatis" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Spartieji klaviatūros klavišai, skirti įjungti laikmačiui. Įveskite naują " #~ "sparčiųjų klavišų kombinaciją, kad pakeistumėte." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoro kiekis prieš ilgąją pertrauką" #~ msgid "Keyboard shortcut" #~ msgstr "Spartieji klaviatūros klavišai" #~ msgid "Screen notifications" #~ msgstr "Ekrano pranešimai" #~ msgid "Wait for activity after a break" #~ msgstr "Laukti aktyvumo po pertraukos" #~ msgid "Plugins…" #~ msgstr "Įskiepiai…" #~ msgid "Plugins" #~ msgstr "Įskiepiai" #~ msgid "Back" #~ msgstr "Grįžti" #~ msgid "Complete a few sessions" #~ msgstr "Užbaikite keletą seansų" #~ msgid "Previous (Alt+Left)" #~ msgstr "Ankstesnis (Alt + kairėn)" #~ msgid "Next (Alt+Right)" #~ msgstr "Kitas (Alt + dešinėn)" #~ msgid "Complete" #~ msgstr "Užbaigta" #~ msgid "Enable" #~ msgstr "Įjungti" #~ msgid "Add" #~ msgstr "Pridėti" #~ msgid "Remove" #~ msgstr "Pašalinti" #~ msgid "Elapsed Time" #~ msgstr "Praėjęs laikas" #~ msgid "Pause Timer" #~ msgstr "Pristabdyti laikmatį" #~ msgid "Pause break" #~ msgstr "Pauzė" #~ msgid "Pause Pomodoro" #~ msgstr "Pauzė" #~ msgid "Resume break" #~ msgstr "Tęsti" #~ msgid "Resume Pomodoro" #~ msgstr "Tęsti" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "Liko %d minutė" #~ msgstr[1] "Liko %d minutės" #~ msgstr[2] "Liko %d minučių" #~ msgid "Report issue" #~ msgstr "Pranešti problemą" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Nepavyko paleisti %s serviso" #~ msgid "Woodland Birds" #~ msgstr "Miško paukščiai" #~ msgid "End of Break Sound" #~ msgstr "Pertraukos pabaigos garsas" #~ msgid "Start of Break Sound" #~ msgstr "Pertraukos pradžios garsas" #~ msgid "Off" #~ msgstr "Išjungta" #~ msgid "Ticking sound" #~ msgstr "Tiksėjimo garsas" #~ msgid "Start of break sound" #~ msgstr "Pertraukos pradžios garsas" #~ msgid "End of break sound" #~ msgstr "Petraukos pabaigos garsas" #~ msgid "Focus on your task." #~ msgstr "Susikaupkite ties savo užduotimi." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Jūs turite %d minutę" #~ msgstr[1] "Jūs turite %d minutes" #~ msgstr[2] "Jūs turite %d minučių" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Jūs turite %d sekundę" #~ msgstr[1] "Jūs turite %d sekundes" #~ msgstr[2] "Jūs turite %d sekundžių" #~ msgid "Take a longer break" #~ msgstr "Daryti ilgesnę pertrauką" #~ msgid "Lengthen it" #~ msgstr "Prailginti" #~ msgid "Shorten it" #~ msgstr "Sutrumpinti" #~ msgid "Start pomodoro" #~ msgstr "Pradėti pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "\"%s\" sparčiosios klaviatūros klavišų kombinacijos naudojimas gali " #~ "kirstis su rašymu. Pabandykite panaudoti kitą klavišą, pavyzdžiui: " #~ "Control, Alt arba Shift." #~ msgid "Available" #~ msgstr "Pasiekiama" #~ msgid "Busy" #~ msgstr "Užimta" #~ msgid "Idle" #~ msgstr "Laisva" #~ msgid "Invisible" #~ msgstr "Nematoma" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f v" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f v" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "Remind to take a break" #~ msgstr "Priminti apie pertrauką" #, javascript-format #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d nauja žinutė" #~ msgstr[1] "%d naujos žinutės" #~ msgstr[2] "%d naujų žinučių" #~ msgid "Take a break!" #~ msgstr "Laikas pertraukai!" #, javascript-format #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Jums liko %d minutė iki sekančio pomodoro." #~ msgstr[1] "Jums liko %d minutės iki sekančio pomodoro." #~ msgstr[2] "Jums liko %d minučių iki sekančio pomodoro." #, javascript-format #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Jums liko %d sekundė iki sekančio pomodoro." #~ msgstr[1] "Jums liko %d sekundės iki sekančio pomodoro." #~ msgstr[2] "Jums liko %d sekundžių iki sekančio pomodoro." #~ msgid "Hey!" #~ msgstr "Labas!" #~ msgid "You're missing out on a break" #~ msgstr "Jūs praleidžiate pertrauką" #~ msgid "It seems to be uninstalled" #~ msgstr "Panašu, kad tai ištrinta" #~ msgid "Extension is out of date" #~ msgstr "Pasenęs įskiepis" #~ msgid "Upgrade" #~ msgstr "Atnaujinti" #~ msgid "Remove Sound" #~ msgstr "Pašalinti garsą:" focustimerhq-FocusTimer-8581be2/po/meson.build000066400000000000000000000001041520625676500214320ustar00rootroot00000000000000i18n = import('i18n') i18n.gettext(gettext_package, preset: 'glib') focustimerhq-FocusTimer-8581be2/po/nb.po000066400000000000000000001661311520625676500202440ustar00rootroot00000000000000# Norwegian translation for focus-timer # Copyright (c) 2020 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Arno Teigseth , 2020. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-25 14:40+0100\n" "Last-Translator: Arno Teigseth \n" "Language-Team: Norwegian\n" "Language: nb\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "En produktivitetstidtaker som hjelper deg å jobbe mer effektivt ved å dele " "opp tiden i fokuserte arbeidsøkter etterfulgt av korte pauser. Jobb i 25 " "minutter, og ta deretter en 5-minutters pause for å opprettholde " "konsentrasjonen og forebygge utbrenthet." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Hovedfunksjoner:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Tilpassbare lengder for arbeidsøkter og pauser" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Skjermoverlegg under pauser" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Tidtaker" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Daglig statistikk" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Månedlig statistikk" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Innstillinger" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Skjermoverlegg" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Start eller Stopp" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Start, Pause eller Fortsett" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Start Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Start" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stopp" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pause" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Fortsett" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Hopp over" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Spol tilbake" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Forleng gjeldende pomodoro eller pause" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Tilbakestill" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Vis innstillinger" #: src/application.vala:203 msgid "Quit application" msgstr "Avslutt programmet" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Vis versjonsinformasjon og avslutt" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Hent til forgrunnen" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s igjen" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Egendefinert handling \"%s\" feilet" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Tidsavbrudd nådd" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Kunne ikke utføre kommando" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Kommandoen er tom" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Ulukket hermetegn" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Ugyldig kommando" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Ukjent variabel \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Ukjent format \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programmet \"%s\" ble ikke funnet" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Handlinger" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Nedtelling" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Økt" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Annet" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Startet tidtakeren." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Stoppet tidtakeren manuelt." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Nedtellingen har blitt satt på pause manuelt. Utløses ikke ved låsing av " "skjermen eller ved dvalemodus." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "Nedtellingen har blitt gjenopptatt manuelt." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Hoppet til neste tidsblokk før nedtellingen var ferdig." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Tilbakespoling har blitt brukt. Det legger til en pause bakover i tid." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Tømte økten manuelt." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Ferdig" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Nedtellingen er ferdig. Hvis det ventes på bekreftelse, kan varigheten av " "tidsblokken fortsatt endres." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Endret" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Utløses ved enhver endring relatert til nedtellingen." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Bekreft fremdrift" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "En manuell bekreftelse kreves for å starte neste tidsblokk." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Gått videre" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Gikk over til eller hoppet over til neste tidsblokk." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Tilstand endret" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Gikk over til neste tidsblokk eller når en pause endrer navn." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Planlagt på nytt" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Utløses når planlagte tidsblokker har endret seg." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Utløpt" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "Utløses når økten er i ferd med å bli tilbakestilt på grunn av inaktivitet." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Ta en pause" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Ta en liten pause" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Ta en lang pause" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro snart slutt" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Ta en pause" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Pausen er snart slutt" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minutt" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Gjør deg klar…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "Pomodoro er ferdig!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "Pausen er over!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "Bekreft start av en Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Bekreft start av en pause…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "Bekreft start av en kort pause…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "Bekreft start av en lang pause…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Hopp over pause" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "Kunne ikke starte avspilling" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "Filen ble ikke funnet" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "Filtypen støttes ikke" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Stoppet" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pause" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Kort pause" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Lang pause" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%ut" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u time" msgstr[1] "%u timer" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minutt" msgstr[1] "%u minutter" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekund" msgstr[1] "%u sekunder" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "Det nøyaktige tidspunktet for gjeldende hendelse." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Gjeldende fase i Pomodoro-syklusen. Mulige verdier: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status for gjeldende tidsblokk. Mulige verdier: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "Et flagg som indikerer om nedtellingen har startet." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "Et flagg som indikerer om nedtellingen er satt på pause." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "Et flagg som indikerer om nedtellingen er ferdig." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "Et flagg som indikerer om tidtakeren teller ned aktivt." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "Varigheten på gjeldende nedtelling." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "Avvik mellom medgått tid og tiden som har passert." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "Tiden som er brukt på nedtellingen." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "Tiden som er igjen før nedtellingen slutter." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "Tidspunktet når nedtellingen startet." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell-utvidelse" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "Få den beste opplevelsen!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Aktiver GNOME Shell-utvidelse for sømløs integrasjon" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "Alltid innen rekkevidde" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "Kontroller tidtakeren direkte fra topplinjen uten å åpne appen" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "Færre distraksjoner" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "Forbedrede pausepåminnelser" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Elegant fullskjermsoverlegg som gjør det mer behagelig å ta pauser" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "Klar til å prøve?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "_Installer utvidelse" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "_Ikke nå" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Noe gikk galt" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "Kopier til utklippstavle" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "_Prøv igjen" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Avbryt" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "Tidsavbrudd nådd" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "Installasjon av utvidelser er ikke tillatt" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Kunne ikke laste ned utvidelsen" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Skjermoverlegg" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Skrivebord" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell-utvidelse er tilgjengelig" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "Lær mer" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Ubrukt" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "Ferdig!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistikk" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Avslutt" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "Logg" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "Tom logg" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "Oppføringer vil vises her når du starter tidtakeren." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "Kontekst" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Kommando" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "Utdata" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "Feil" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "Avslutningskode:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "Kjøretid:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Arno Teigseth " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "Doner" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pauser" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "Avbrytelser" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Pauseandel" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dag" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Uke" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Måned" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Ingenting å se her ennå" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "Fullfør noen Pomodoroer for å fylle denne!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Hoppet over %u dag" msgstr[1] "Hoppet over %u dager" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Hoppet over %u uke" msgstr[1] "Hoppet over %u uker" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Hoppet over %u måned" msgstr[1] "Hoppet over %u måneder" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "I dag" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "I går" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Denne uken" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Uke %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "Uke %u av %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Kort pause" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Lang pause" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pause" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "Åpne skjermoverlegg" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "Økten har utløpt" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Lang pause om %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Spol tilbake ett minutt" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "_Kompakt visning" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Innstillinger" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Om" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Avslutt" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "Hovedmeny" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "La tidtakeren fortsette?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Du kan la den kjøre i bakgrunnen — varsler og hurtigtaster vil fortsatt " "virke." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Kjør i bakgrunnen" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "På tide med en pause" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Hovedvindu" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "Foretrekk mørkt tema" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "Foretrekk kompakt visning" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Startet" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Satt på pause" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Rediger egendefinert handling" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "A_vbryt" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "_Lagre" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Navn" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Utløser" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "Hendelse" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "Utfør kommando etter en hendelse." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "Vilkår" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Sikre kjøring av en andre kommando når vilkåret ikke lenger er oppfylt." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "Hendelser" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "Legg til _hendelse" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "_Filter" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell-kommando" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Kommandoer" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "Kommando ved oppfylt vilkår" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "Kommando ved ikke oppfylt vilkår" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "Arbeidsmappe" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Bruk undershell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "Kjør programmet fra et undershell som f.eks. sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "Send inndata" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "I stedet for å sende variabler kan du prosessere et JSON-objekt." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "Vent på fullføring" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "Blokker kjøring av andre kommandoer til kommandoen er ferdig." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Slett handling" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "Ingen hendelser er spesifisert ennå." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Legg til egendefinert handling" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "_Legg til" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "Velg arbeidsmappe" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Velg" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Navnløs handling" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Legg til vilkår" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Legg til gruppe" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "OG" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ELLER" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "Er" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "Er ikke" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "Er lik" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "Større enn" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "Mindre enn" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "Ja" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Nei" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minutter" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "Sekunder" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "Timer" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "Velg felt…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Status" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Kjører" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Varighet" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "Sett inn variabel" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "_Logg" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "Vis kjørelogg" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Kjør shell-kommandoer automatisk ved tidtakerhendelser eller vilkår. Lær mer." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Angi hurtigtast" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "_Angi" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Deaktivert" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Trykk Esc for å avbryte eller Rettetast for å deaktivere " "hurtigtasten" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globale hurtigtaster lar deg kontrollere appen selv når den ikke er på " "skjermen. De virker så lenge appen kjører i bakgrunnen." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "Åpne appinnstillinger for å redigere globale hurtigtaster" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "_Rediger" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "Tast inn ny hurtigtast for å starte eller stoppe tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Tast inn ny hurtigtast for å starte/pause/fortsette tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Tast inn ny hurtigtast for å starte tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Tast inn ny hurtigtast for å stoppe tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Tast inn ny hurtigtast for å pause tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Tast inn ny hurtigtast for å fortsette tidtakeren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "Tast inn ny hurtigtast for å hoppe over" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "Spol tilbake ett minutt" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "Tast inn ny hurtigtast for å spole tilbake" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "Tast inn ny hurtigtast for å hente vinduet til forgrunnen" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "Kunngjøringer" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "Tiden er i ferd med å gå ut" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Varsle når Pomodoro eller pause er i ferd med å slutte." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "Et fullskjermsvarsel som er ment for å tvinge deg til å ta en pause." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "Forsinkelse for låsing" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "Periode med inaktivitet før skjermen låses." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "Forsinkelse for gjenåpning" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Periode med inaktivitet før overlegget gjenåpnes etter at det er fjernet." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "Aldri" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Varsler" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Lyder" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Utseende" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Hurtigtaster" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automasjon" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "Lyder er deaktivert" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Varsellyder" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Lyd ved ferdig Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Lyd ved ferdig pause" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Bakgrunnslyd" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Bjelle" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Kraftig bjelle" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tikking av klokke" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "Ingen" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volum:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Velg egendefinert lyd" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Varighet for Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Varighet for kort pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Varighet for lang pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Antall sykluser" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "Oppførsel" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "Pause ved låsing av skjermen" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "Bekreft start av en pause" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Bekreft start av en Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "En enkelt økt vil ta %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% av tiden vil bli satt av til pauser." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Bruk endringer på pågående Pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Bruk endringer på pågående pause?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Bruk" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Sidestolpe" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Verktøy for tidsstyring" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Behold fokus ved å ta hyppige pauser" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Visuelle varsler og lydvarsler" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Tidssporing og statistikk" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "GNOME skrivebordsintegrasjon" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Kjør egendefinerte kommandoer etter Pomodoro eller pause" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Kompakt tidtaker" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Lagt til tamilsk oversettelse (takk til @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Lagt til hebraisk oversettelse (takk til @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Støtte for GNOME Shell 49 (takk til @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Oppdatert tysk oversettelse (takk til @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Støtte for GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Delt tidsbruk over midnatt" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Lagt til telugu oversettelse (takk til @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Støtte for GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Tillat å fjerne skjermoverlegg med bevegelse når en video spilles" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Lagt til georgisk oversettelse (takk til @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Justerte oversettelser i appdata (takk til @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Rettet at varsel ble liggende etter forlengelse av Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Rettelser for GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Fjernet støtte for GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Oversikt over endringer i gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Støtte for GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Justerte byggeskript til meson 0.59.0 (takk til @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "La Pomodoro håndtere systemvarsler mens tidtakeren kjører" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 sekunder" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 sekunder" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minutt" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minutter" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minutter" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minutter" #~ msgid "Timer Ticking" #~ msgstr "Klikking av tidtaker" #, fuzzy #~ msgid "Birds" #~ msgstr "Fugler" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;tidtaker;nedtelling;" #~ msgid "Start/Stop" #~ msgstr "Start/Stopp" #~ msgid "Pause/Resume" #~ msgstr "Pause/Fortsett" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Hopp til en pomodoro eller til en pause" #~ msgid "Reset current session" #~ msgstr "Tilbakestill gjeldende økt" #~ msgid "Run as background service" #~ msgstr "Kjør som bakgrunnstjeneste" #~ msgid "About Pomodoro" #~ msgstr "Om Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Et enkelt pauseverktøy" #, fuzzy #~ msgid "_Stopped" #~ msgstr "_Stoppet" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Utvidelse for GNOME Shell er tilgjengelig" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Kunne ikke installere utvidelse" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Lang pause om %s" #~ msgid "A time management utility for GNOME" #~ msgstr "Et pauseverktøy for GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Et GNOME-verktøy som hjelper deg å ta pauser etter Pomodoro-teknikken. " #~ "Det har til hensikt å øke produktiviteten og fokuset ved å ta korte " #~ "pauser etter hver 25-minutters arbeidsøkt." #~ msgid "Timer window" #~ msgstr "Tidtakervindu" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Indikator for GNOME-skallet" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Støtte for GNOME Shell 42 (@milotype og @kappa)" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Støtte for GNOME Shell 41 (@mbooth101)" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Støtte GNOME Shell 40.0, ikke 4.0" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Støtte for GNOME Shell 4.0" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Støtte for GNOME Shell 3.38 (@ignapk og @szpak)" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Støtte for GNOME Shell 3.36" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Støtte bare for GNOME Shell 3.34" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Støtte for GNOME Shell 3.32 (@demokritos)" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Støtte for GNOME Shell 3.28 og 3.30 (@aerostitch)" #~ msgid "_Timer" #~ msgstr "_Tidtaker" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Hurtigtast for å starte/stoppe tidtakeren. Trykk ny hurtigtast for å " #~ "endre." #~ msgid "Pomodoros before a long break" #~ msgstr "Antall Pomodoro før lang pause" #~ msgid "Keyboard shortcut" #~ msgstr "Hurtigtast" #~ msgid "Screen notifications" #~ msgstr "Skjermvarsler" #~ msgid "Wait for activity after a break" #~ msgstr "Vent på aktivitet etter pause" #~ msgid "Plugins…" #~ msgstr "Plugins…" #~ msgid "Plugins" #~ msgstr "Plugins" #~ msgid "Back" #~ msgstr "Tilbake" #~ msgid "Complete a few sessions" #~ msgstr "Fullfør et par økter" #~ msgid "Previous (Alt+Left)" #~ msgstr "Forrige (Alt+Venstre)" #~ msgid "Next (Alt+Right)" #~ msgstr "Neste (Alt+Høyre)" #~ msgid "Complete" #~ msgstr "Ferdig" #~ msgid "Enable" #~ msgstr "Slå på" #~ msgid "Add" #~ msgstr "Legg til" #~ msgid "Remove" #~ msgstr "Fjern" #~ msgid "Elapsed Time" #~ msgstr "Forløpt tid" #~ msgid "Pause Timer" #~ msgstr "Pause tidtaker" #~ msgid "Pause break" #~ msgstr "Pause" #~ msgid "Pause Pomodoro" #~ msgstr "Pause" #~ msgid "Resume break" #~ msgstr "Fortsett" #~ msgid "Resume Pomodoro" #~ msgstr "Fortsett" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minutt igjen" #~ msgstr[1] "%d minutter igjen" #~ msgid "Report issue" #~ msgstr "Rapporter et problem" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Kunne ikke kjøre tjenesten %s" #~ msgid "Woodland Birds" #~ msgstr "Skogsfugler" #~ msgid "End of Break Sound" #~ msgstr "Lyd ved slutt på pause" #~ msgid "Start of Break Sound" #~ msgstr "Lyd ved start på pause" #~ msgid "Off" #~ msgstr "Av" #~ msgid "Ticking sound" #~ msgstr "Klikkelyd" #~ msgid "Start of break sound" #~ msgstr "Lyd ved start på pause" #~ msgid "End of break sound" #~ msgstr "Lyd ved slutt på pause" #~ msgid "Focus on your task." #~ msgstr "Fokuser på oppgaven." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Du har %d minutt" #~ msgstr[1] "Du har %d minutter" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Du har %d sekund" #~ msgstr[1] "Du har %d sekunder" #~ msgid "Take a longer break" #~ msgstr "Ta en lengre pause" #~ msgid "Lengthen it" #~ msgstr "Forleng den" #~ msgid "Shorten it" #~ msgstr "Forkort den" #~ msgid "Start pomodoro" #~ msgstr "Start pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Hurtigtasten \"%s\" vil forstyrre skrivinga. Prøv å legge til en annen " #~ "tast, som Kontroll, Alt eller Shift." #~ msgid "Available" #~ msgstr "Tilgjengelig" #~ msgid "Busy" #~ msgstr "Opptatt" #~ msgid "Idle" #~ msgstr "Inaktiv" #~ msgid "Invisible" #~ msgstr "Usynlig" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f t" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f t" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Statistikk" #~ msgid "It seems to be uninstalled" #~ msgstr "Ser ut til at det er avinstallert" #~ msgid "Extension is out of date" #~ msgstr "Utvidelsen er utgått på dato" #~ msgid "Upgrade" #~ msgstr "Oppgrader" focustimerhq-FocusTimer-8581be2/po/nl.po000066400000000000000000002007011520625676500202460ustar00rootroot00000000000000# Dutch translation for focus-timer # Copyright (c) 2017 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Johannes Kool <100johannes100@gmail.com>, 2017. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-26 11:34+0200\n" "Last-Translator: Heimen Stoffels \n" "Language-Team: Dutch\n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Een productiviteitstimer die u helpt effectiever te werken door uw tijd op " "te delen in gefocuste werksessies gevolgd door korte pauzes. Werk 25 minuten " "en neem daarna 5 minuten pauze om de concentratie vast te houden en burn-out " "te voorkomen." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Belangrijkste kenmerken:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Aanpasbare duur van werksessies en pauzes" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Schermoverlay tijdens pauzes" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Timer" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Dagelijkse statistieken" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Maandelijkse statistieken" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Voorkeuren" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Schermoverlay" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 #, fuzzy msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "Russische vertaling bijgewerkt (met dank aan @rkaverin)" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Starten of stoppen" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Starten, onderbreken of hervatten" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Pomodoro starten" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Starten" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stoppen" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Onderbreken" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Hervatten" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Overslaan" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Terugspoelen" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Huidige pomodoro of pauze verlengen" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Resetten" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Voorkeuren tonen" #: src/application.vala:203 msgid "Quit application" msgstr "Toepassing afsluiten" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Versie-informatie tonen en afsluiten" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Op de voorgrond plaatsen" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "nog %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Aangepaste actie \"%s\" is mislukt" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Time-out bereikt" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Opdracht uitvoeren mislukt" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "Opdracht is leeg" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Niet-gesloten aanhalingsteken" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Ongeldige opdracht" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Onbekende variabele \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Onbekend formaat \"%s\"" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Programma \"%s\" niet gevonden" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Acties" #: src/core/event.vala:183 msgid "Countdown" msgstr "Aftellen" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Sessie" #: src/core/event.vala:189 msgid "Other" msgstr "Overig" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Timer gestart." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "De timer is handmatig gestopt." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Het aftellen is handmatig onderbroken. Dit wordt niet geactiveerd bij " "schermvergrendeling of systeem-slaapstand." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Het aftellen is handmatig hervat." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "Naar het volgende tijdsblok gesprongen voordat het aftellen klaar was." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "Terugspoelactie is gebruikt. Het voegt een pauze toe in het verleden." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Sessie handmatig gewist." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Voltooid" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Het aftellen is voltooid. Indien wordt gewacht op bevestiging, kan de duur " "van het tijdsblok nog worden gewijzigd." #: src/core/event.vala:333 msgid "Changed" msgstr "Gewijzigd" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "Geactiveerd bij elke wijziging gerelateerd aan het aftellen." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Bevestig voortgang" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "" "Handmatige bevestiging is vereist om het volgende tijdsblok te starten." #: src/core/event.vala:350 msgid "Advanced" msgstr "Gevorderd" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Overgegaan naar of overgeslagen naar een volgend tijdsblok." #: src/core/event.vala:358 msgid "State Changed" msgstr "Status gewijzigd" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Overgegaan naar een volgend tijdsblok of herlabelen van een pauze." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Opnieuw gepland" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Geactiveerd wanneer geplande tijdsblokken zijn gewijzigd." #: src/core/event.vala:374 msgid "Expired" msgstr "Verstreken" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "Geactiveerd wanneer de sessie wordt gereset wegens inactiviteit." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Neem pauze" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Neem een korte pauze" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Neem een lange pauze" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "De Pomodoro loopt bijna af" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Neem een pauze" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "De pauze loopt bijna af" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuut" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Maak je gereed…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "De Pomodoro is voorbij!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "De pauze is voorbij!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Bevestig de start van een Pomodoro…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Bevestig de start van een pauze…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Bevestig de start van een korte pauze…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Bevestig de start van een lange pauze…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Pauze overslaan" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Initialiseren van afspelen mislukt" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Bestand niet gevonden" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Bestandstype niet ondersteund" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Gestopt" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pauze" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Korte pauze" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Lange pauze" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%u u" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%u m" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u uur" msgstr[1] "%u uur" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuut" msgstr[1] "%u minuten" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u seconde" msgstr[1] "%u seconden" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Het exacte tijdstip van de huidige gebeurtenis." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "De huidige fase van de Pomodoro-cyclus. Mogelijke waarden: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status van het huidige tijdsblok. Mogelijke waarden: scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Een vlag die aangeeft of het aftellen is begonnen." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Een vlag die aangeeft of het aftellen is onderbroken." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Een vlag die aangeeft of het aftellen is voltooid." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Een vlag die aangeeft of de timer actief aan het aftellen is." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Duur van het huidige aftellen." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Verschil tussen de verstreken tijd en de werkelijk gepasseerde tijd." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "De hoeveelheid tijd gespendeerd aan het aftellen." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "De resterende tijd voordat het aftellen eindigt." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Tijdstip waarop het aftellen is gestart." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "GNOME Shell-uitbreiding" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Krijg de beste ervaring!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Schakel de GNOME Shell-uitbreiding in voor naadloze integratie" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Altijd binnen handbereik" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "Bedien de timer direct vanuit de bovenbalk zonder de app te openen" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Minder afleidingen" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Verfijnde pauze-herinneringen" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegante schermvullende overlay die het nemen van pauzes aangenamer maakt" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Klaar om het te proberen?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "Uitbreiding _installeren" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_Niet nu" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Er is iets misgegaan" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Kopiëren naar klembord" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "Opnieuw _proberen" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Afbreken" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Time-out bereikt" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Installeren van uitbreidingen is niet toegestaan" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Downloaden van uitbreiding mislukt" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Schermoverlay" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Bureaublad" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "GNOME Shell-uitbreiding beschikbaar" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Meer informatie" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Niet gebruikt" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Klaar!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistieken" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Afsluiten" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Logboek" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Logboek legen" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Items verschijnen hier zodra u de timer start." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Context" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Opdracht" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Uitvoer" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Fout" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Afsluitcode:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Uitvoeringstijd:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "Johannes Kool\n" "Heimen Stoffels " #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Doneren" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pauzes" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Onderbrekingen" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Pauzeratio" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dag" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Week" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Maand" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Nog niets te zien hier" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Voltooi een paar Pomodoro's om dit te vullen!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u dag overgeslagen" msgstr[1] "%u dagen overgeslagen" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u week overgeslagen" msgstr[1] "%u weken overgeslagen" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u maand overgeslagen" msgstr[1] "%u maanden overgeslagen" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Vandaag" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Gisteren" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Deze week" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Week %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Week %u van %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Korte pauze" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Lange pauze" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pauze" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Schermoverlay openen" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Sessie is verlopen" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Lange pauze over %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Eén minuut terugdraaien" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_Compacte weergave" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Voorkeuren" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Over" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Afsluiten" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Hoofdmenu" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Timer laten lopen?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "U kunt de timer in de achtergrond laten lopen — meldingen en sneltoetsen " "blijven dan werken." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "In de achtergrond uitvoeren" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Het is tijd voor pauze" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Hoofdvenster" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Voorkeur voor donker thema" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Voorkeur voor compacte weergave" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Gestart" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Onderbroken" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Aangepaste actie bewerken" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Annuleren" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Opslaan" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Naam" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Trigger" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Gebeurtenis" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Voer opdracht uit na een gebeurtenis." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Voorwaarde" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Zorg voor uitvoering van een tweede opdracht zodra aan de voorwaarde niet " "meer wordt voldaan." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Gebeurtenissen" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "_Gebeurtenis toevoegen" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filteren" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell-opdracht" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Opdrachten" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Opdracht bij voldane voorwaarde" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Opdracht bij niet-voldane voorwaarde" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Werkmap" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Subshell gebruiken" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Programma uitvoeren vanuit een subshell zoals sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Invoergegevens doorgeven" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "In plaats van variabelen kunt u een JSON-object verwerken." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Wachten op voltooiing" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Andere opdrachten blokkeren totdat de opdracht is voltooid." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "Actie _verwijderen" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Nog geen gebeurtenissen gespecificeerd." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Aangepaste actie toevoegen" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Toevoegen" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Werkmap selecteren" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Selecteren" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Naamloze actie" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "Voorwaarde toevoegen" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "Groep toevoegen" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "EN" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "OF" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Is" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Is niet" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Gelijk aan" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Groter dan" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Kleiner dan" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Ja" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Nee" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minuten" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Seconden" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Uur" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Veld selecteren…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Status" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Lopend" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Duur" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Variabele invoegen" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Opmaak" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Logboek" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Uitvoeringslogboek tonen" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Voer automatisch shell-opdrachten uit bij timer-gebeurtenissen of " "voorwaarden. Meer informatie." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Sneltoets instellen" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Instellen" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Uitgeschakeld" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Druk op Esc om te annuleren of op Backspace om de sneltoets " "uit te schakelen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globale sneltoetsen laten u de app bedienen, zelfs wanneer deze niet in " "beeld is. Ze werken zolang de app in de achtergrond draait." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "App-instellingen openen om globale sneltoetsen te bewerken" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_Bewerken" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Voer nieuwe sneltoets in voor starten of stoppen van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "" "Voer nieuwe sneltoets in voor starten/onderbreken/hervatten van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Voer nieuwe sneltoets in voor starten van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Voer nieuwe sneltoets in voor stoppen van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Voer nieuwe sneltoets in voor onderbreken van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Voer nieuwe sneltoets in voor hervatten van de timer" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Voer nieuwe sneltoets in voor overslaan" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Eén minuut terugdraaien" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Voer nieuwe sneltoets in voor terugdraaien" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Voer nieuwe sneltoets in om venster op de voorgrond te plaatsen" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Aankondigingen" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Tijd loopt af" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Melden wanneer Pomodoro of pauze bijna voorbij is." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Een schermvullende melding bedoeld om een pauze af te dwingen." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Vergrendelingsvertraging" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Inactiviteitsduur om het scherm te vergrendelen." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Heropenvertraging" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Inactiviteitsduur om de overlay opnieuw te tonen na sluiten." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Nooit" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Meldingen" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "Geluiden" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Uiterlijk" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Sneltoetsen" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automatisering" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Geluiden zijn uitgeschakeld" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Waarschuwingsgeluiden" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Geluid bij voltooide Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Geluid bij voltooide pauze" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Achtergrondgeluid" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Bel" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Harde bel" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tikkende klok" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Geen" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volume:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Aangepast geluid selecteren" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Duur van Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Duur van korte pauze" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Duur van lange pauze" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Aantal cycli" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Gedrag" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Onderbreken bij schermvergrendeling" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Starten van pauze bevestigen" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Starten van Pomodoro bevestigen" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Een enkele sessie duurt %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% van de tijd wordt gereserveerd voor pauzes." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "Wijzigingen toepassen op huidige Pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "Wijzigingen toepassen op huidige pauze?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Toepassen" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Zijbalk" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Hulpmiddel voor tijdsbeheer" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Blijf gefocust door regelmatig pauzes te nemen" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Visuele en geluidsmeldingen" #~ msgid "Time tracking and statistics" #~ msgstr "Tijdregistratie en statistieken" #~ msgid "GNOME desktop integration" #~ msgstr "Integratie met GNOME-werkomgeving" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Voer aangepaste opdrachten uit na een pomodoro of pauze" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Compacte timer" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Tamil-vertaling toegevoegd (met dank aan @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Hebreeuwse vertaling toegevoegd (met dank aan @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Ondersteuning voor GNOME Shell 49 (met dank aan @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Duitse vertaling bijgewerkt (met dank aan @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Ondersteuning voor GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Splits gespendeerde tijd rond middernacht" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Telugu-vertaling toegevoegd (met dank aan @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Ondersteuning voor GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Toestaan om schermoverlay te sluiten met gebaren wanneer een video speelt" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Georgische vertaling toegevoegd (met dank aan @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Vertalingen in appdata aangepast (met dank aan @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Probleem opgelost waarbij melding bleef staan na verlengen Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Oplossingen voor GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Ondersteuning voor GNOME Shell 45 beëindigd" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Overzicht van wijzigingen in gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Ondersteuning voor GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Bouwscript aangepast voor meson 0.59.0 (met dank aan @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Laat Pomodoro de systeemmeldingen beheren terwijl de timer loopt" #~ msgid "15 seconds" #~ msgstr "15 seconden" #~ msgid "30 seconds" #~ msgstr "30 seconden" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuut" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minuten" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minuten" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minuten" #~ msgid "Timer Ticking" #~ msgstr "Tikkende timer" #~ msgid "Birds" #~ msgstr "Vogels" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "timer;tijdklok;tijdindeling;pomodoro;pauze;" #~ msgid "Start/Stop" #~ msgstr "Starten/Stoppen" #~ msgid "Pause/Resume" #~ msgstr "Onderbreken/Hervatten" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Doorgaan naar een pomodoro of pauze" #~ msgid "Reset current session" #~ msgstr "Huidige sessie resetten" #~ msgid "Run as background service" #~ msgstr "Uitvoeren als achtergronddienst" #~ msgid "About Pomodoro" #~ msgstr "Over Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Een eenvoudige tijdindelingstoepassing" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Stoppen" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Ondersteuning voor GNOME Shell 3.36" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "De uitbreiding kan niet worden ingeschakeld" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Duur van lange pauze" #~ msgid "A time management utility for GNOME" #~ msgstr "Een tijdindelingstoepassing voor GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Een GNOME-hulptoepassing die u helpt tijd in te delen volgens de Pomodoro-" #~ "techniek. Deze techniek is bedoeld om uw productiviteit en focus te " #~ "verbeteren door telkens na 25 minuten werken korte pauzes te nemen." #~ msgid "Timer window" #~ msgstr "Tijdklokvenster" #~ msgid "Indicator for GNOME Shell" #~ msgstr "GNOME Shell-indicator" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.24.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.24.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.23.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.23.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.22.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.22.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Updated Brazilian translation (thanks @costaronaldo)" #~ msgstr "Catalaanse vertaling bijgewerkt (met dank aan @antoniofsm)" #, fuzzy #~ msgid "Updated Chinese translation (thanks @HaorongX)" #~ msgstr "Chinese vertaling bijgewerkt (met dank aan @wffger)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.21.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.21.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "" #~ "Ondersteuning voor GNOME Shell 3.38 (met dank aan @ignapk en @szpak)" #, fuzzy #~ msgid "Added Croatian translation (thanks @dayeondev)" #~ msgstr "Noorse vertaling (met dank aan @arnotixe)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.20.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Ondersteuning voor GNOME Shell 3.32 (met dank aan @demokritos)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.2" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Updated Russian translation (@prokoudine)" #~ msgstr "Russische vertaling bijgewerkt (met dank aan @rkaverin)" #, fuzzy #~ msgid "Updated Dutch translation (@Vistaus)" #~ msgstr "Duitse vertaling bijgewerkt (met dank aan @tsabsch)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Ondersteuning voor GNOME Shell 3.34" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.19.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Ondersteuning voor GNOME Shell 3.36" #, fuzzy #~ msgid "Changed blur effect during break" #~ msgstr "Achtergrondvervaging tijdens pauzes" #, fuzzy #~ msgid "Added Korean translation (@dayeondev)" #~ msgstr "Noorse vertaling (met dank aan @arnotixe)" #, fuzzy #~ msgid "Updated Brazilian translation (@alexandreafa)" #~ msgstr "Catalaanse vertaling bijgewerkt (met dank aan @antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.18.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.18.0" #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "" #~ "Ondersteuning voor GNOME Shell 3.38 (met dank aan @ignapk en @szpak)" #~ msgid "Removed ayatana-appindicator3 support" #~ msgstr "ayatana-appindicator3-ondersteuning verwijderd" #~ msgid "Added Norwegian translation (@arnotixe)" #~ msgstr "Noorse vertaling (met dank aan @arnotixe)" #~ msgid "Added Finnish translation (@iqqmuT)" #~ msgstr "Finse vertaling (met dank aan @iqqmuT)" #~ msgid "Updated Indonesian translation (@atriwidada)" #~ msgstr "Indonesische vertaling bijgewerkt (met dank aan @atriwidada)" #~ msgid "Updated Chinese translation (@wffger)" #~ msgstr "Chinese vertaling bijgewerkt (met dank aan @wffger)" #~ msgid "Updated Russian translation (@rkaverin)" #~ msgstr "Russische vertaling bijgewerkt (met dank aan @rkaverin)" #~ msgid "Updated French translation (@precondition)" #~ msgstr "Franse vertaling bijgewerkt (met dank aan @precondition)" #~ msgid "Overview of changes in gnome-pomodoro 0.17.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.17.0" #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Ondersteuning voor GNOME Shell 3.36" #~ msgid "Updated Catalan translation (@antoniofsm)" #~ msgstr "Catalaanse vertaling bijgewerkt (met dank aan @antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.16.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.16.0" #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Ondersteuning voor GNOME Shell 3.34" #~ msgid "Added esperanto translation (@SeZuo)" #~ msgstr "Esperanto-vertaling (met dank aan @SeZuo)" #~ msgid "Moved app-menu to main window" #~ msgstr "Het toepassingsmenu is verplaatst naar het hoofdvenster" #~ msgid "Overview of changes in gnome-pomodoro 0.15.1" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.1" #~ msgid "Minor code cleanups" #~ msgstr "Enkele codeverbeteringen" #~ msgid "Overview of changes in gnome-pomodoro 0.15.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.15.0" #~ msgid "Minor code cleanups to support ES6 syntax" #~ msgstr "Enkele codeverbeteringen ten behoeve van de ES6-syntax" #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Ondersteuning voor GNOME Shell 3.32 (met dank aan @demokritos)" #~ msgid "Fix for build with vala 0.44.1 (@snizovtsev)" #~ msgstr "" #~ "Oplossing voor het compileren met vala 0.44.1 (met dank aan @snizovtsev)" #~ msgid "Updated German translation (@c7hm4r)" #~ msgstr "Duitse vertaling bijgewerkt (met dank aan @c7hm4r)" #~ msgid "Fix for handle error recreating existing folder (@Rj7)" #~ msgstr "" #~ "Oplossing voor behandelingsfout omtrent het opnieuw aanmaken van een " #~ "bestaande map (met dank aan @Rj7)" #~ msgid "Overview of changes in gnome-pomodoro 0.14.0" #~ msgstr "Wijzigingslog van gnome-pomodoro 0.14.0" #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "" #~ "Ondersteuning voor GNOME Shell 3.28 en 3.30 (met dank aan @aerostitch)" #~ msgid "Background blur under the dialog during breaks" #~ msgstr "Achtergrondvervaging tijdens pauzes" #~ msgid "Updated German translation (@tsabsch)" #~ msgstr "Duitse vertaling bijgewerkt (met dank aan @tsabsch)" #~ msgid "Updated Russian translation (@tigertv)" #~ msgstr "Russische vertaling bijgewerkt (met dank aan @tigertv)" #~ msgid "_Timer" #~ msgstr "_Tijdklok" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Sneltoets om de tijdklok aan of uit te zetten. Druk op een nieuwe " #~ "toetscombinatie om te veranderen." #~ msgid "Pomodoros before a long break" #~ msgstr "Pomodoro's voorafgaand aan een lange pauze" #~ msgid "Keyboard shortcut" #~ msgstr "Sneltoets" #~ msgid "Screen notifications" #~ msgstr "Schermmeldingen" #~ msgid "Wait for activity after a break" #~ msgstr "Wachten op activiteit na pauzes" #~ msgid "Plugins…" #~ msgstr "Plug-ins…" #~ msgid "Plugins" #~ msgstr "Plug-ins" #~ msgid "Back" #~ msgstr "Terug" #~ msgid "Complete a few sessions" #~ msgstr "Rond een paar sessies af" #~ msgid "Previous (Alt+Left)" #~ msgstr "Vorige (Alt+pijltje naar links)" #~ msgid "Next (Alt+Right)" #~ msgstr "Vorige (Alt+pijltje naar rechts)" #~ msgid "Complete" #~ msgstr "Afronden" #~ msgid "Enable" #~ msgstr "Inschakelen" #~ msgid "Add" #~ msgstr "Toevoegen" #~ msgid "Remove" #~ msgstr "Verwijderen" #~ msgid "Elapsed Time" #~ msgstr "Verstreken tijd" #~ msgid "Pause Timer" #~ msgstr "Tijdklok onderbreken" #~ msgid "Pause break" #~ msgstr "Onderbreken" #~ msgid "Pause Pomodoro" #~ msgstr "Onderbreken" #~ msgid "Resume break" #~ msgstr "Hervatten" #~ msgid "Resume Pomodoro" #~ msgstr "Hervatten" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "Nog %d minuut te gaan" #~ msgstr[1] "Nog %d minuten te gaan" #~ msgid "Report issue" #~ msgstr "Probleem melden" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "%s kan niet worden uitgevoerd" #~ msgid "Woodland Birds" #~ msgstr "Bosvogels" #~ msgid "End of Break Sound" #~ msgstr "Geluid aan het einde van de pauze" #~ msgid "Start of Break Sound" #~ msgstr "Geluid aan het begin van de pauze" #~ msgid "Off" #~ msgstr "Uit" #~ msgid "Ticking sound" #~ msgstr "Getik" #~ msgid "Start of break sound" #~ msgstr "Geluid aan het begin van de pauze" #~ msgid "End of break sound" #~ msgstr "Geluid aan het einde de pauze" #~ msgid "Focus on your task." #~ msgstr "Focus op je taak." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Je hebt nog %d minuut" #~ msgstr[1] "Je hebt nog %d minuten" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Je hebt nog %d seconde" #~ msgstr[1] "Je hebt nog %d seconden" #~ msgid "Take a longer break" #~ msgstr "Neem een langere pauze" #~ msgid "Lengthen it" #~ msgstr "Verlengen" #~ msgid "Shorten it" #~ msgstr "Inkorten" #~ msgid "Start pomodoro" #~ msgstr "Pomodoro starten" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "De toets ‘%s’ wordt gebruikt om te typen. Probeer een tweede toets toe te " #~ "voegen, zoals Ctrl, Alt of Shift." #~ msgid "Available" #~ msgstr "Beschikbaar" #~ msgid "Busy" #~ msgstr "Bezig" #~ msgid "Idle" #~ msgstr "Inactief" #~ msgid "Invisible" #~ msgstr "Onzichtbaar" #, c-format #~ msgid "%d m" #~ msgstr "%d min." #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f uur" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f uur" #~ msgid "Extension is out of date" #~ msgstr "De uitbreiding is verouderd" #~ msgid "Upgrade" #~ msgstr "Bijwerken" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "Remind to take a break" #~ msgstr "Aan pauze herinneren" #, javascript-format #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d nieuw bericht" #~ msgstr[1] "%d nieuwe berichten" #~ msgid "Take a break!" #~ msgstr "Neem een pauze!" #, javascript-format #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Je hebt nog %d minuut tot de volgende pomodoro." #~ msgstr[1] "Je hebt nog %d minuten tot de volgende pomodoro." #, javascript-format #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Je hebt nog %d seconde tot de volgende pomodoro." #~ msgstr[1] "Je hebt nog %d seconden tot de volgende pomodoro." #~ msgid "Hey!" #~ msgstr "Hallo!" #~ msgid "You're missing out on a break" #~ msgstr "Je mist een pauze!" #~ msgid "It seems to be uninstalled" #~ msgstr "De plugin lijkt verwijderd te zijn" #~ msgid "Remove Sound" #~ msgstr "Geluid verwijderen" focustimerhq-FocusTimer-8581be2/po/pl.po000066400000000000000000002021271520625676500202540ustar00rootroot00000000000000# Polish translation for focus-timer # Copyright (c) 2012 - 2025 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Kamil Prusko , 2012 -2026. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 09:26+0200\n" "PO-Revision-Date: 2026-05-29 10:56+0200\n" "Last-Translator: Kamil Prusko \n" "Language-Team: Polish\n" "Language: pl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " "|| n%100>=20) ? 1 : 2);\n" "X-Generator: Gtranslator 50.0\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "Minutnik Koncentracji" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "Pracuj z regularnymi przerwami" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "pomodoro;minutnik;produktywność;śledzenie czasu;zarządzanie czasem;" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Minutnik pomaga pracować efektywniej, dzieląc czas na bloki skupionej pracy " "oraz krótkie przerwy. Cykl 25 minut pracy i 5 minut przerwy pomaga utrzymać " "koncentrację i zapobiega zmęczeniu." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Kluczowe funkcje:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Dowolne długości pracy i przerw" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Nakładka ekranowa podczas przerw" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "Ikona w zasobniku" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "Klawisze skrótu (skróty globalne)" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "Statystyki dzienne, tygodniowe i miesięczne" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "Możliwość rozszerzenia przez własne polecenia powłoki, D-Bus i CLI" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "Rozszerzenie GNOME Shell dla głębszej integracji z pulpitem" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Minutnik" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Statystyki dzienne" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Statystyki miesięczne" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferencje" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Nakładka ekranowa" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "Przegląd zmian w focus-timer 1.1.0:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "Wsparcie dla rozszerzenia GNOME Shell" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "Opcja automatycznego uruchamiania przy logowaniu" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "Zweryfikowane pliki dźwiękowe" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "Naprawiono kompatebilność z vala 0.56.19" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "Przegląd zmian w focus-timer 1.0:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" "Naprawiono skalowanie nakładki przerwy na wyświetlaczach HiDPI " "(podziękowania dla @scholzri)" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "Automatyczna codzienna kopia zapasowa" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "Usunięto libcanberra do odtwarzania dźwięków powiadomień" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "Zaktualizowano tłumaczenie litewskie (podziękowania dla @psukys)" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "Zaktualizowano tłumaczenie rosyjskie (podziękowania dla @ViktorOn)" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "Rozpocznij lub zatrzymaj" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "Rozpocznij, wstrzymaj lub wznów" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Rozpocznij pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "Rozpocznij przerwę" #: src/application.vala:167 msgid "Start short break" msgstr "Rozpocznij krótką przerwę" #: src/application.vala:170 msgid "Start long break" msgstr "Rozpocznij długą przerwę" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Rozpocznij" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Zatrzymaj" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Wstrzymaj" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Wznów" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Pomiń" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Przewiń" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "SEKUNDY" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Przedłuż obecne pomodoro lub przerwę" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Resetuj" #: src/application.vala:197 msgid "Print timer status" msgstr "Wyświetl stan minutnika" #: src/application.vala:200 msgid "Show preferences" msgstr "Pokaż preferencje" #: src/application.vala:203 msgid "Quit application" msgstr "Zakończ aplikację" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Wyświetl wersję aplikacji" #: src/application.vala:240 msgid "Timer Options:" msgstr "Opcje Minutnika:" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "Wyświetla opcje sterowania minutnikiem" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "Błędy można zgłaszać pod adresem: %s" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Przywołaj okno na wierzch" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "Pozostało %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" "Nieprawidłowe użycie. Przekaż jedną flagę do sterowania minutnikiem na raz." #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "Akcja \"%s\" nie powiodła się" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Osiągnięto limit czasu" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "Nie udało się wykonać polecenia" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "Polecenie jest puste" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Niezamknięty cudzysłów" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Nieprawidłowe polecenie" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Nieznana zmienna \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Nieznany format \"%s\"" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Program \"%s\" nie został znaleziony" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Akcje" #: src/core/event.vala:183 msgid "Countdown" msgstr "Odliczanie" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Sesja" #: src/core/event.vala:189 msgid "Other" msgstr "Inne" #: src/core/event.vala:269 msgid "Started the timer." msgstr "Uruchomiono minutnik." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "Minutnik został zatrzymany ręcznie." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Odliczanie zostało wstrzymane ręcznie. Nie jest wywoływane podczas " "blokowania ekranu lub wstrzymywania systemu." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Odliczanie zostało wznowione ręcznie." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "" "Przeskoczono do następnego bloku czasowego przed zakończeniem odliczania." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Użyto akcji przewijania, która działa przez dodanie przerwy w przeszłości." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Porzucono sesję na życzenie." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Zakończono" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Zakończono odliczanie. Czas końca bloku czasowego może zostać zmodyfikowany, " "gdy przejście do kolejnego bloku zostanie potwierdzone." #: src/core/event.vala:333 msgid "Changed" msgstr "Zmieniono" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "Wywoływane przy każdej zmianie dotyczącej odliczania czasu." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Potwierdź przejście" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "" "Wymagane jest ręczne potwierdzenie, aby rozpocząć następny blok czasowy." #: src/core/event.vala:350 msgid "Advanced" msgstr "Przejście" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Przejście lub przeskoczenie do następnego bloku czasowego." #: src/core/event.vala:358 msgid "State Changed" msgstr "Zmiana stanu" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "" "Przejście do następnego bloku czasowego lub gdy przerwa zostaje " "przemianowana." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Zmiana planu" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Wywoływane, gdy zaplanowane bloki czasowe zostały zmienione." #: src/core/event.vala:374 msgid "Expired" msgstr "Wygaśnięcie" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "Wywołane przy wygaśnięciu sesji spowodowane brakiem aktywności." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Zrób przerwę" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Zrób krótką przerwę" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Zrób długą przerwę" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro za chwilę się skończy" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "Zrób przerwę" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Przerwa za chwilę się skończy" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 minuta" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Przygotuj się…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Pomodoro się skończyło!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "Przerwa się skończyła!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Potwierdź rozpoczęcie pomodoro…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Potwierdź rozpoczęcie przerwy…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Potwierdź rozpoczęcie krótkiej przerwy…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Potwierdź rozpoczęcie długiej przerwy…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Pomiń Przerwę" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Nie udało się zainicjować odtwarzania" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Plik nie został znaleziony" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Typ pliku nie jest obsługiwany" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "Zatrzymany" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Przerwa" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Krótka Przerwa" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Długa Przerwa" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%ug" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u godzina" msgstr[1] "%u godziny" msgstr[2] "%u godzin" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuta" msgstr[1] "%u minuty" msgstr[2] "%u minut" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekunda" msgstr[1] "%u sekundy" msgstr[2] "%u sekund" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Dokładny czas bieżącego zdarzenia." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Bieżąca faza cyklu Pomodoro. Możliwe wartości: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status bieżącego bloku czasowego. Możliwe wartości: scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Flaga wskazująca, czy odliczanie zostało rozpoczęte." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Flaga wskazująca, czy odliczanie jest wstrzymane." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Flaga wskazująca, czy odliczanie zostało zakończone." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Flaga wskazująca, czy minutnik odlicza czas." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Czas trwania bieżącego odliczania." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "" "Różnica między czasem, który upłynął na minutniku, a rzeczywistym czasem." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "Wartość czasu od rozpoczęcia odliczania." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "Wartość czasu do końca odliczania." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Czas rozpoczęcia odliczania." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "Rozszerzenie GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Uzyskaj najlepsze doświadczenie!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Włącz rozszerzenie GNOME Shell dla płynnej integracji z pulpitem" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Zawsze w zasięgu ręki" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "" "Steruj minutnikiem bezpośrednio z górnego paska bez otwierania aplikacji" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Mniej rozproszeń" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" "Pozwól by Minutnik Koncentracji zarządzał powiadomieniami systemowymi " "w trakcie działania" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Powiadomienia o przerwie" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegancka nakładka ekranowa, która czyni robienie przerw przyjemniejszym" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Gotowy aby wypróbować?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "_Zainstaluj rozszerzenie" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_Nie teraz" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Coś poszło nie tak" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Kopiuj do schowka" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Spróbuj ponownie" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "_Przerwij" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Osiągnięto limit czasu" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Instalowanie rozszerzeń jest niedozwolone" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "Nie udało się pobrać rozszerzenia" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "Wskaźnik" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "Ikona" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "Tekst" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "Wyświetl Jako" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Nakładka Ekranowa" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "Efekt Rozmycia" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "Gest Odrzucenia" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Pulpit" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "Zainstaluj" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "Zaktualizuj" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "Wyloguj się by zakończyć aktualizację" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "Brak Wsparcia" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "Zarządzaj Powiadomieniami" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "Aktywuj tryb Nie Przeszkadzać podczas Pomodoro." #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "Rozszerzenie GNOME Shell jest dostępne" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Czytaj więcej" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "Ustawienia" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" "Zezwól na powiadomienia w trybie „Nie przeszkadzać” i wyłącz ich historię, " "aby aplikacja działała niezawodnie." #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "Nieużywane" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Zakończono!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "%u z %u" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statystyki" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Zakończ" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "Zrób przerwę" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "Ikona w Zasobniku" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "Pokaż Ikonę w Zasobniku" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "Zamknięcie okna sprawia, że aplikacja nadal działa w tle." #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Dziennik" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Pusty dziennik" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Wpisy pojawią się tutaj po uruchomieniu minutnika." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Kontekst" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Polecenie" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Wyjście" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Błąd" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Kod Zakończenia:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Czas Wykonania:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Kamil Prusko" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Przekaż darowiznę" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "Przerwy" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Rozproszenia" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "Stosunek przerw" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dzień" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Tydzień" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Miesiąc" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "Nic tu jeszcze nie ma" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Ukończ kilka pomodoro, aby zobaczyć podsumowanie!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Pominięty %u dzień" msgstr[1] "Pominięte %u dni" msgstr[2] "Pominiętych %u dni" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Pominięty %u tydzień" msgstr[1] "Pominięte %u tygodnie" msgstr[2] "Pominiętych %u tygodni" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Pominięty %u miesiąc" msgstr[1] "Pominięte %u miesiące" msgstr[2] "Pominiętych %u miesięcy" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Dzisiaj" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Wczoraj" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Ten tydzień" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "Tydzień %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Tydzień %u z %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Krótka Przerwa" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Długa Przerwa" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_Przerwa" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Otwórz nakładkę ekranową" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Sesja wygasła" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "Długa przerwa za %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "Przewiń jedną minutę" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_Widok kompaktowy" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferencje" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_O programie" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "Za_kończ" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Menu główne" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Czy minutnik ma nadal odliczać?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Może on działać w tle — powiadomienia i skróty klawiszowe nadal będą działać." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "Uruchom w tle" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Czas na zrobienie przerwy" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "Okno Główne" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Preferuj ciemny motyw" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Preferuj widok kompaktowy" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "Rozpoczęty" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Wstrzymane" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "Edytuj akcję" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Anuluj" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Zapisz" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nazwa" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "Wyzwalacz" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Zdarzenie" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Wykonaj polecenie po zdarzeniu." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Warunek" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Zapewnij, że zostanie wykonane drugie polecenie, gdy warunek przestanie być " "spełniony." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Zdarzenia" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Dodaj _Zdarzenie" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filtr" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filtr" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "Polecenie powłoki" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "Polecenia" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Polecenie Spełnienia Warunku" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Polecenie Zerwania Warunku" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Ścieżka Robocza" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Użyj Podpowłoki" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Odpal polecenie używając powłoki np. sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Przekaż Dane na Wejście" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "" "Zamiast przekazywać argumenty dla polecenia, możesz przkazać dane JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Czekaj Na Zakońcenie" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Opóźnij wykonanie innych poleceń, aż to zostanie ukończone." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "_Usuń Akcję" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Nie wybrano żadnego zdarzenia." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "Dodaj akcję…" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Dodaj" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Wybierz Ścieżkę Roboczą" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Wybierz" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "Nienazwana akcja" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Dodaj Warunek" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Dodaj Grupę" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "I" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "LUB" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Jest" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Nie Jest" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Wynosi" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Większe Niż" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Mniejsze Niż" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Tak" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Nie" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "Minuty" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Sekundy" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Godziny" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Wybierz Pole…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Stan" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Odlicza" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "Długość" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Wstaw Zmienną" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Dziennik" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Pokaż dziennik" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "Odpal polecenia automatycznie wg wydarzeń lub warunków." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "Automatyczne Uruchamianie" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "Odpal aplikację przy starcie systemu." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" "Aplikacja uruchomi się w tle. Możesz używać wskaźnika i skrutów klawiszowych." #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "Przypisz Skrót" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Przypisz" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Wyłączony" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "Wciśnij Esc by anulować, lub Backspace by odpiąć skrót" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Skróty globalne pozwalają sterować aplikacją nawet wtedy, gdy nie jest ona " "widoczna na ekranie. Działają tak długo, jak długo aplikacja działa w tle." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Otwórz ustawienia aplikacji, aby edytować globalne skróty" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_Edytuj" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Podaj skrót dla rozpoczęcia lub zatrzymania odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Podaj skrót dla rozpoczęcia/wstrzymania/wznowienia odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Podaj skrót dla rozpoczęcia odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Podaj skrót dla zatrzymania odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Podaj skrót dla wstrzymania odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Podaj skrót dla wznowienia odliczania" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Podaj skrót dla pominięcia pomodoro lub przerwy" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Przewiń Jedną Minutę" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Podaj skrót dla przewinięcia minutnika" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Podaj skrót by przywołać okno minutnika na wierzch" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Zapowiedzi" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "O Kończącym Się Czasie" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "Powiadom gdy pomodoro lub przerwa za chwilę się skończy." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Powiadomienie pełno-ekranowe skłaniające do zrobienia przerwy." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Opóżnienie Blokady" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Czas nieaktywności do zablokowania ekranu." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Czas Do Ponowienia" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Czas nieaktywności do ponownego pokazania nakładki." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Nigdy" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Powiadomienia" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Dźwięki" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Wygląd" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "Skróty Klawiszowe" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "Powiązania" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "Automatyzacja" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Dźwięki Są Wyłączone" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "Dźwięki Powiadomień" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Dźwięk Zakończenia Pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "Dźwięk Zakończenia Przerwy" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "Dźwięk w Tle" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Dzwonek" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Głośny Dzwonek" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tykanie Zegara" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "Meteronom" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "Szum Brązowy" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Brak" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Głośność:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Wybierz Własny Dźwięk" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Długość Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "Długość Krótkiej Przerwy" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "Długość Długiej Przerwy" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Liczba Cykli" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Zachowanie" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Wstrzymaj Odliczanie, Blokując Ekran" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Potwierdź Rozpoczęcie Przerwy" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Potwierdź Rozpoczęcie Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Łączny czas pojedyńczej sesji: %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% czasu będzie przeznaczone na przerwy." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Zastosować zmiany do obecnego pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Zastosować zmiany do obecnej przerwy?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "Zastosuj" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Panel boczny" #~ msgid "Time management utility" #~ msgstr "Narzędzie do zarządzania czasem" #~ msgid "pomodoro;timer;" #~ msgstr "pomodoro;minutnik;timer;" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Zachowaj skupienie robiąc częste przerwy" #~ msgid "Visual and audio notifications" #~ msgstr "Powiadomienia dźwiękowe i na ekranie" #~ msgid "Time tracking and statistics" #~ msgstr "Śledzenie czasu i statystyki" #~ msgid "GNOME desktop integration" #~ msgstr "Integracja z pulpitem GNOME" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Odpalanie własnych poleceń" #~ msgid "15 seconds" #~ msgstr "15 sekund" #~ msgid "30 seconds" #~ msgstr "30 sekund" #~ msgid "1 minute" #~ msgstr "1 minuta" #~ msgid "2 minutes" #~ msgstr "2 minuty" #~ msgid "3 minutes" #~ msgstr "3 minuty" #~ msgid "5 minutes" #~ msgstr "5 minut" #~ msgid "Compact timer" #~ msgstr "Kompaktowy minutnik" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.28.1" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Dodano tłumaczenie tamilskie (podziękowania dla @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Dodano tłumaczenie hebrajskie (podziękowania dla @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.28.0" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Wsparcie dla GNOME Shell 49 (podziękowania dla @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Zaktualizowano tłumaczenie niemieckie (podziękowania dla @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.27.0" #~ msgid "Support for GNOME Shell 48" #~ msgstr "Wsparcie dla GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Podzielenie czasu spędzonego po północy" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Dodano tłumaczenie telugu (podziękowania dla @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.26.0" #~ msgid "Support for GNOME Shell 47" #~ msgstr "Wsparcie dla GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Zezwól na odrzucenie nakładki ekranowej gestem podczas odtwarzania wideo" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Dodano tłumaczenie gruzińskie (podziękowania dla @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Dostosowano tłumaczenia w appdata (podziękowania dla @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Naprawiono zachowywanie powiadomienia po przedłużeniu pomodoro" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.25.1" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Poprawki dla GNOME Shell 46" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Porzucono wsparcie dla GNOME Shell 45" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Przegląd zmian w gnome-pomodoro 0.25.0" #~ msgid "Support for GNOME Shell 46" #~ msgstr "Wsparcie dla GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Dostosowano skrypt budowania do meson 0.59.0 (podziękowania dla @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Pozwól Pomodoro zarządzać powiadomieniami systemowymi podczas " #~ "działania minutnika" #~ msgid "Timer Ticking" #~ msgstr "Tykanie Stopera" #~ msgid "Birds" #~ msgstr "Ptaki" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "minutnik;" #~ msgid "Start/Stop" #~ msgstr "Rozpocznij/Zatrzymaj" #~ msgid "Pause/Resume" #~ msgstr "Wstrzymaj/Wznów" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Przejdź do pomodoro lub do przerwy" #~ msgid "Reset current session" #~ msgstr "Zresetuj bieżącą sesję" #~ msgid "Run as background service" #~ msgstr "Uruchom w tle" #~ msgid "About Pomodoro" #~ msgstr "O Pomodoro" #~ msgid "A simple time management utility" #~ msgstr "Proste narzędzie do zarządzania czasem" #, fuzzy #~ msgid "_Stopped" #~ msgstr "Zatrzymaj" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Wskaźnik dla GNOME Shell" #, fuzzy #~ msgid "_Install" #~ msgstr "Zainstaluj" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "Nie udało się włączyć wtyczki" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "Długość długiej przerwy" #~ msgid "A time management utility for GNOME" #~ msgstr "Narzędzie do zarządzania czasem dla pulpitu GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Narzędzie dla pulpitu GNOME do zarządzania czasem techniką Pomodoro. " #~ "Dzięki robieniu regularnych przerw ma na celu zmniejszyć zmęczenie i " #~ "poprawić produktywność." #~ msgid "Timer window" #~ msgstr "Okno minutnika" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Wskaźnik dla GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Wskaźnik dla GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Wskaźnik dla GNOME Shell" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Wskaźnik dla GNOME Shell" #~ msgid "_Timer" #~ msgstr "_Minutnik" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Skrót klawiszowy do aktywowania minutnika. Wciśnij nową kombinację " #~ "klawiszy by zmienić." #~ msgid "Pomodoros before a long break" #~ msgstr "Liczba pomodoro w cyklu" #~ msgid "Keyboard shortcut" #~ msgstr "Skrót klawiszowy" #~ msgid "Screen notifications" #~ msgstr "Powiadomienia ekranowe" #~ msgid "Wait for activity after a break" #~ msgstr "Po przerwie zaczekaj na aktywność" #~ msgid "Plugins…" #~ msgstr "Wtyczki…" #~ msgid "Plugins" #~ msgstr "Wtyczki" #~ msgid "Back" #~ msgstr "Cofnij" #~ msgid "Complete a few sessions" #~ msgstr "Ukończ parę sesji" #~ msgid "Previous (Alt+Left)" #~ msgstr "Wstecz (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "Dalej (Alt+Right)" #~ msgid "Complete" #~ msgstr "Ukończenie" #~ msgid "Enable" #~ msgstr "Włącz" #~ msgid "Add" #~ msgstr "Dodaj" #~ msgid "Remove" #~ msgstr "Usuń" #~ msgid "Elapsed Time" #~ msgstr "Upłunięty Czas" #~ msgid "Pause Timer" #~ msgstr "Wstrzymaj Minutnik" #~ msgid "Pause break" #~ msgstr "Wstrzymaj przerwę" #~ msgid "Pause Pomodoro" #~ msgstr "Wstrzymaj Pomodoro" #~ msgid "Resume break" #~ msgstr "Wznów przerwę" #~ msgid "Resume Pomodoro" #~ msgstr "Wznów Pomodoro" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d minuta do końca" #~ msgstr[1] "%d minuty do końca" #~ msgstr[2] "%d minut do końca" #~ msgid "Report issue" #~ msgstr "Zgłoś problem" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Nie udało się uruchomić usługi %s" #~ msgid "Woodland Birds" #~ msgstr "Głosy Ptaków" #~ msgid "End of Break Sound" #~ msgstr "Dźwięk zakończenia przerwy" #~ msgid "Start of Break Sound" #~ msgstr "Dźwięk rozpoczęcia przerwy" #~ msgid "Off" #~ msgstr "Wyłączone" #~ msgid "Ticking sound" #~ msgstr "Tykanie" #~ msgid "Start of break sound" #~ msgstr "Dźwięk rozpoczęcia przerwy" #~ msgid "End of break sound" #~ msgstr "Dźwięk zakończenia przerwy" #~ msgid "Focus on your task." #~ msgstr "Skup się na pracy." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "Została %d minuta" #~ msgstr[1] "Zostały %d minuty" #~ msgstr[2] "Zostało %d minut" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "Została %d sekunda" #~ msgstr[1] "Zostały %d sekundy" #~ msgstr[2] "Zostało %d sekund" #~ msgid "Take a longer break" #~ msgstr "Weź dłuższą przerwę" #~ msgid "Lengthen it" #~ msgstr "Wydłuż przerwę" #~ msgid "Shorten it" #~ msgstr "Skróć przerwę" #~ msgid "Start pomodoro" #~ msgstr "Zacznij pomodoro" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Skrót \"%s\" będzie kolidował z pisaniem. Spróbuj innej kombinacji np. " #~ "dodając Control, Alt bądź Shift." #~ msgid "Available" #~ msgstr "Dostępny" #~ msgid "Busy" #~ msgstr "Zajęty" #~ msgid "Idle" #~ msgstr "Niedostępny" #~ msgid "Invisible" #~ msgstr "Niewidoczny" #, c-format #~ msgid "%d m" #~ msgstr "%d m" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f g" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f g" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Statystyki" #~ msgid "It seems to be uninstalled" #~ msgstr "Wygląda że nie jest zainstalowana" #~ msgid "Extension is out of date" #~ msgstr "Wtyczka jest zbyt stara" #~ msgid "Upgrade" #~ msgstr "Zaktualizuj" #~ msgid "Remind to take a break" #~ msgstr "Przypominaj o przerwie" #~ msgid "%d new message" #~ msgid_plural "%d new messages" #~ msgstr[0] "%d nowa wiadomość" #~ msgstr[1] "%d nowe wiadomości" #~ msgstr[2] "%d nowych wiadomości" #~ msgid "Take a break!" #~ msgstr "Przerwa!" #~ msgid "You have %d minute until next pomodoro." #~ msgid_plural "You have %d minutes until next pomodoro." #~ msgstr[0] "Została %d minuta do następnego pomodoro." #~ msgstr[1] "Zostały %d minuty do następnego pomodoro." #~ msgstr[2] "Zostało %d minut do następnego pomodoro." #~ msgid "You have %d second until next pomodoro." #~ msgid_plural "You have %d seconds until next pomodoro." #~ msgstr[0] "Została %d sekunda do następnego pomodoro." #~ msgstr[1] "Zostały %d sekundy do następnego pomodoro." #~ msgstr[2] "Zostało %d sekund do następnego pomodoro." #~ msgid "Hey!" #~ msgstr "Hej!" #~ msgid "You're missing out on a break" #~ msgstr "Omija cię przerwa." #~ msgid "Remove Sound" #~ msgstr "Usuń Dźwięk" #~ msgid "time;timer;tasks;manage;organize;" #~ msgstr "czas;minutnik;zadania;organizuj;" #~ msgid "%ds" #~ msgstr "%ds" #~ msgid "during pomodoro" #~ msgstr "podczas pomodoro" #~ msgid "during break" #~ msgstr "podczas przerwy" #~ msgid "Indicator for Pomodoro will show up after you restart your desktop." #~ msgstr "Wskaźnik Pomodoro pokaże się po zrestartowaniu pulpitu." #~ msgid "Extension does not support shell version" #~ msgstr "Rozszerzenie nie wspiera twojej wersji GNOME Shell`a" #~ msgid "You need to upgrade Pomodoro." #~ msgstr "Musisz zaktualizować Pomodoro." #~ msgid "Could not find extension \"%s\" in \"%s\"" #~ msgstr "Nie znalazło rozszerzenia \"%s\" w \"%s\"" #~ msgid "Error loading extension" #~ msgstr "Błąd podczas ładowania rozszerzenia" #~ msgid "Pomodoro extension is disabled" #~ msgstr "Rozszerzenie Pomodoro jest wyłączone" #~ msgid "Extension provides better desktop integration for the pomodoro app." #~ msgstr "Rozszerzenie integruje pulpit z aplikacją Pomodoro." #~ msgid "" #~ "The shortcut \"%s\" cannot be used because it will become impossible to " #~ "type using this key.\n" #~ "Please try with a key such as Control, Alt or Shift at the same time." #~ msgstr "" #~ "Skrót \"%s\" nie może być ustawiony, bo byłoby niemożliwe pisanie z " #~ "wykorzystaniem tego klawisza.\n" #~ "Spróbuj użyć dodatkowo Ctrl, Alt lub Shift." #~ msgid "On" #~ msgstr "Włączone" #~ msgid "Change Presence Status" #~ msgstr "Zmień status dostępności" #~ msgid "Status during pomodoro" #~ msgstr "Status podczas pomodoro" #~ msgid "Set custom status" #~ msgstr "Ustaw własny status" #~ msgid "Authenticate" #~ msgstr "Uwierzytelnij" #~ msgid "Wake up screen" #~ msgstr "Wybudź ekran" #~ msgid "Select sound for end of break" #~ msgstr "Wybierz dźwięk zakończenia przerwy" #~ msgid "Short Text" #~ msgstr "Krótki tekst" #~ msgid "Presence" #~ msgstr "Dostępność" #~ msgid "Change presence status" #~ msgstr "Zmień status dostępności" #~ msgid "_No sound" #~ msgstr "_Bez dźwięku" #~ msgid "_Open" #~ msgstr "_Otwórz" #~ msgid "All files" #~ msgstr "Wszystkie pliki" #~ msgid "Supported audio files" #~ msgstr "Obsługiwane pliki audio" #~ msgid "A new pomodoro is starting" #~ msgstr "Czas wrócić do pracy" #~ msgid "Could not run pomodoro" #~ msgstr "Błąd przy uruchomieniu pomodoro" #~ msgid "Looks like gnome-pomodoro is not installed" #~ msgstr "Wygląda, że nie masz zainstalowanego gnome-pomodoro" #~ msgid "" #~ "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; either version 3 of the License, or (at your " #~ "option) any later version." #~ msgstr "" #~ "Niniejszy program jest wolnym oprogramowaniem; można go rozprowadzać " #~ "dalej i/lub modyfikować na warunkach Powszechnej Licencji Publicznej GNU, " #~ "wydanej przez Fundację Wolnego Oprogramowania (Free Software Foundation) " #~ "- według wersji trzeciej tej Licencji lub którejś z późniejszych wersji." #~ msgid "Select sound for pomodoro start" #~ msgstr "Wybierz dźwięk rozpoczęcia pomodoro" #~ msgid "Postpone pomodoro when idle" #~ msgstr "Wydłuż przerwę przy braku aktywności" #, fuzzy #~ msgid "" #~ "System notifications including chat messages won't show up during " #~ "pomodoro." #~ msgstr "" #~ "Powiadomienia systemowe, łącznie z powiadomieniami komunikatora,będą " #~ "ukryte podczas pomodoro." #, fuzzy #~ msgid "" #~ "System notifications including chat messages won't show up during break." #~ msgstr "" #~ "Powiadomienia systemowe, łącznie z powiadomieniami komunikatora,będą " #~ "ukryte podczas przerwy." #, fuzzy #~ msgid "System notifications including chat messages won't show up." #~ msgstr "" #~ "Powiadomienia systemowe, łącznie z powiadomieniami komunikatora,będą " #~ "ukryte." #~ msgid "OK" #~ msgstr "OK" #~ msgid "Manage your time and tasks" #~ msgstr "Zarządzaj czasem i zadaniami" #~ msgid "Time in seconds you are supposed to be working." #~ msgstr "Czas w sekundach, w którym niby masz pracować." #~ msgid "Time in seconds you are supposed to have a short break." #~ msgstr "Czas w sekundach, w którym niby masz mieć krótką przerwę." #~ msgid "Time in seconds you are supposed to have a longer break." #~ msgstr "Czas w sekundach, w którym niby masz mieć długą przerwę." #~ msgid "Whether to show a notification dialog when pause starts." #~ msgstr "Otwiera pełnoekranowe okienko do powiadomienia o przerwie." #~ msgid "Whether you are not using a computer to work." #~ msgstr "Dostosuje powiadomienia do pracy biurowej." #~ msgid "Whether to play a sound to notify of events." #~ msgstr "Czy odtwarzyć dźwięk pod koniec przerwy." #~ msgid "Notification sound file" #~ msgstr "Plik dźwiękowy powiadomień" #~ msgid "Number of completed sessions since long break" #~ msgstr "Liczba ukończonych sesji od długiej przerwy" #~ msgid "Timer toggle key" #~ msgstr "Skrót klawiszowy" focustimerhq-FocusTimer-8581be2/po/pt_BR.po000066400000000000000000001523611520625676500206530ustar00rootroot00000000000000# Brazilian Portuguese translation for focus-timer # Copyright (c) 2012 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Alê Borba , 2013. # Matheus Cansian , 2014. # Ronaldo Costa , 2022. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-26 11:39+0200\n" "Last-Translator: Ronaldo Costa \n" "Language-Team: Brazilian Portuguese\n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Um temporizador de produtividade que ajuda você a trabalhar de forma mais " "eficaz, dividindo seu tempo em sessões de trabalho focado seguidas por " "pausas curtas. Trabalhe por 25 minutos e faça uma pausa de 5 minutos para " "manter a concentração e evitar o esgotamento." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "Recursos principais:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "Duração de sessões de trabalho e pausas personalizável" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "Sobreposição de tela durante as pausas" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Temporizador" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "Estatísticas diárias" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "Estatísticas mensais" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Preferências" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "Sobreposição de tela" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "Iniciar ou Parar" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "Iniciar, Interromper ou Continuar" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Iniciar Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Iniciar" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Parar" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Interromper" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Continuar" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Pular" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "Voltar" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Estender o pomodoro ou pausa atual" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "Redefinir" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Exibir preferências" #: src/application.vala:203 msgid "Quit application" msgstr "Encerrar o programa" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Exibir informação da versão e sair" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "Trazer para o Foco" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s restante" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "Ação personalizada \"%s\" falhou" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "Tempo limite atingido" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "Falha ao executar comando" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "O comando está vazio" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "Aspas não fechadas" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "Comando inválido" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "Variável desconhecida \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "Formato desconhecido \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "Programa \"%s\" não encontrado" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Ações" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "Contagem regressiva" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "Sessão" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "Outros" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "Temporizador iniciado." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "Temporizador parado manualmente." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "A contagem foi interrompida manualmente. Não acionado ao bloquear a tela ou " "suspender o sistema." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "A contagem foi continuada manualmente." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "Pulou para o próximo bloco antes do fim da contagem." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "Ação de voltar usada. Adiciona uma interrupção no passado." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "Sessão limpa manualmente." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "Finalizado" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "A contagem terminou. Se aguardando confirmação, a duração do bloco ainda " "pode ser alterada." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "Alterado" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "Acionado em qualquer mudança na contagem regressiva." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "Confirmar avanço" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "Confirmação manual necessária para iniciar o próximo bloco." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "Avançado" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "Transição ou salto para o próximo bloco de tempo." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "Estado alterado" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Transição para o próximo bloco ou renomeação de pausa." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "Reagendado" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "Acionado quando os blocos de tempo agendados mudam." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "Expirado" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "Acionado quando a sessão está prestes a reiniciar por inatividade." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Faça uma pausa" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Faça uma pausa curta" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Faça uma pausa longa" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "O Pomodoro está prestes a acabar" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "Faça uma Pausa" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "A pausa está prestes a terminar" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 minuto" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Prepare-se…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "O Pomodoro acabou!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "A pausa acabou!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Confirme o início de um Pomodoro…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "Confirme o início de uma pausa…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Confirme o início de uma pausa curta…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Confirme o início de uma pausa longa…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Pular pausa" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Falha ao inicializar reprodução" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Arquivo não encontrado" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Tipo de arquivo não suportado" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "Parado" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Pausa" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Pausa Curta" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Pausa Longa" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u hora" msgstr[1] "%u horas" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minuto" msgstr[1] "%u minutos" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u segundo" msgstr[1] "%u segundos" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "O horário exato do evento atual." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "A fase atual do ciclo Pomodoro. Valores possíveis: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status do bloco de tempo atual. Valores possíveis: scheduled, " "in-progress, completed, uncompleted." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Sinalizador indicando se a contagem regressiva começou." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Sinalizador indicando se a contagem regressiva está interrompida." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Sinalizador indicando se a contagem regressiva terminou." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Sinalizador indicando se o temporizador está contando ativamente." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Duração da contagem regressiva atual." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Discrepância entre o tempo decorrido e o tempo passado." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "A quantidade de tempo gasta na contagem regressiva." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "A quantidade de tempo restante antes do fim da contagem." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Hora em que a contagem regressiva começou." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "Extensão do GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Tenha a melhor experiência!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Ative a extensão do GNOME Shell para integração total com a área de " "trabalho" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Sempre ao alcance" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "Controle o temporizador diretamente da barra superior sem abrir o app" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Menos distrações" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Lembretes de pausa refinados" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Sobreposição elegante em tela cheia para tornar as pausas mais agradáveis" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Pronto para tentar?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "_Instalar Extensão" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "Agora _não" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "Algo deu errado" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Copiar para área de transferência" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Tentar Novamente" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "_Abortar" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Tempo limite atingido" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Instalação de extensões não é permitida" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "Falha ao baixar a extensão" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Sobreposição de Tela" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "Extensão do GNOME Shell disponível" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Saiba Mais" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "Não utilizado" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Finalizado!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Estatísticas" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Sair" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Registro" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Registro Vazio" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "As entradas aparecerão aqui quando você iniciar o temporizador." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Contexto" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Comando" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Saída" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Erro" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Código de Saída:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Tempo de Execução:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "Alexandre Magno (alexandre-mbm),\n" "Alexandre Felipe (alexandreafa),\n" "Ronaldo Costa (costaronaldo)" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Doar" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "Pausas" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Interrupções" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "Taxa de Pausas" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dia" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Semana" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Mês" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "Nada para ver por aqui ainda" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Finalize alguns Pomodoros para preencher isto!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Pulado %u dia" msgstr[1] "Pulados %u dias" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Pulada %u semana" msgstr[1] "Puladas %u semanas" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Pulado %u mês" msgstr[1] "Pulados %u meses" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Hoje" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Ontem" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Esta semana" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "Semana %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Semana %u de %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "Pausa _Curta" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "Pausa _Longa" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "_Pausa" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Abrir sobreposição de tela" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "A sessão expirou" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "Pausa longa em %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "Voltar um minuto" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "Vista _Compacta" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Preferências" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Sobre" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "S_air" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Menu Principal" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Manter temporizador rodando?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Você pode mantê-lo rodando em segundo plano — notificações e atalhos " "continuarão funcionando." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "Rodar em segundo plano" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "É hora de fazer uma pausa" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "Janela Principal" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Preferir Tema Escuro" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Preferir Vista Compacta" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "Iniciado" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Interrompido" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "Editar Ação Personalizada" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Cancelar" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Salvar" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Nome" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "Gatilho" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Evento" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Executar comando após um evento." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Condição" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Garantir execução de um segundo comando quando a condição não for mais " "atendida." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Eventos" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Adicionar _Evento" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filtrar" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filtro" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Comando Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "Comandos" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Comando para Condição Atendida" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Comando para Condição Não Atendida" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Diretório de Trabalho" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Usar Subshell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Executar o programa de um subshell como sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Passar Dados de Entrada" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "Em vez de passar variáveis, você pode processar um objeto JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Esperar Conclusão" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Bloquear execução de outros comandos até este comando completar." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "_Excluir Ação" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Nenhum evento especificado ainda." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "Adicionar Ação Personalizada" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Adicionar" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Selecionar Diretório de Trabalho" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Selecionar" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "Ação sem título" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Adicionar Condição" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Adicionar Grupo" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "E" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "OU" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "É" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Não é" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Igual a" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Maior Que" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Menor Que" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Sim" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "Não" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "Minutos" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Segundos" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Horas" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Selecionar Campo…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Estado" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "Tempo Esgotando" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "Duração" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Inserir Variável" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Formato" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Registro" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Mostrar registro de execução" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Executar comandos shell automaticamente em eventos ou condições. Saiba mais." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "Definir Atalho" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Definir" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "Desativado" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Pressione Esc para cancelar ou Backspace para desativar o " "atalho" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Atalhos globais permitem controlar o app mesmo fora da tela. Funcionam " "enquanto o app roda em segundo plano." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Abrir configurações do sistema para editar atalhos globais" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_Editar" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Digite novo atalho para iniciar ou parar o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Digite novo atalho para iniciar/interromper/continuar o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "Digite novo atalho para iniciar o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "Digite novo atalho para parar o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "Digite novo atalho para interromper o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "Digite novo atalho para continuar o temporizador" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Digite novo atalho para pular" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Voltar um Minuto" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Digite novo atalho para voltar" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Digite novo atalho para focar a janela" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Anúncios" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Tempo Esgotando" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "Notificar quando o Pomodoro ou a pausa estiver prestes a acabar." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Uma notificação em tela cheia para forçar a realização da pausa." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Atraso de Bloqueio" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Período de inatividade para bloquear a tela." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Atraso de Reabertura" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Período de inatividade para reabrir a sobreposição após descartada." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Nunca" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Notificações" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Sons" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "Aparência" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "Atalhos de Teclado" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "Automação" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Sons estão Desativados" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "Sons de Alerta" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "Som de Pomodoro Finalizado" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "Som de Pausa Finalizada" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "Som de Fundo" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Sino" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Sino Alto" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tic-tac de Relógio" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Nenhum" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volume:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Selecionar Som Personalizado" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "Duração do Pomodoro" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "Duração da Pausa Curta" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "Duração da Pausa Longa" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "Número de Ciclos" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Comportamento" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Interromper ao Bloquear a Tela" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Confirmar Início de Pausa" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "Confirmar Início de Pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Uma única sessão levará %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% do tempo será alocado para pausas." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Aplicar mudanças ao Pomodoro em curso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Aplicar mudanças à pausa em curso?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "Aplicar" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "Barra Lateral" #, fuzzy #~ msgid "Time management utility" #~ msgstr "Utilitário de gerenciamento de tempo" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Mantenha o foco fazendo pausas frequentes" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "Notificações visuais e sonoras" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "Monitoramento de tempo e estatísticas" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "Integração com a área de trabalho GNOME" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Executar comandos personalizados após um Pomodoro ou pausa" #, fuzzy #~ msgid "Compact timer" #~ msgstr "Temporizador compacto" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.28.1" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Adicionada tradução em tâmil (obrigado @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Adicionada tradução em hebraico (obrigado @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.28.0" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Suporte para o GNOME Shell 49 (obrigado @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Tradução em alemão atualizada (obrigado @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.27.0" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "Suporte para o GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "Dividir o tempo gasto através da meia-noite" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Adicionada tradução em telugo (obrigado @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.26.0" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "Suporte para o GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Permitir descartar a sobreposição de tela por gesto ao reproduzir vídeo" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Adicionada tradução em georgiano (obrigado @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Ajustadas traduções no appdata (obrigado @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.25.2" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Correção ao manter notificação após estender o Pomodoro" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.25.1" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Correções para o GNOME Shell 46" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Removido suporte para o GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Visão geral das mudanças no gnome-pomodoro 0.25.0" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "Suporte para o GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "" #~ "Ajustado script de compilação para o meson 0.59.0 (obrigado @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Deixe o Pomodoro gerenciar notificações enquanto o temporizador " #~ "corre" #~ msgid "15 seconds" #~ msgstr "15 segundos" #~ msgid "30 seconds" #~ msgstr "30 segundos" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 minuto" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 minutos" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 minutos" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 minutos" #~ msgid "Timer Ticking" #~ msgstr "Tic-tac de Temporizador" #~ msgid "Birds" #~ msgstr "Pássaros" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "temporizador;timer;cronômetro;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Iniciar/Parar" #~ msgid "Pause/Resume" #~ msgstr "Interromper/Continuar" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Pular para um pomodoro ou para uma pausa" #~ msgid "Reset current session" #~ msgstr "Redefinir sessão atual" #~ msgid "This month" #~ msgstr "Este mês" focustimerhq-FocusTimer-8581be2/po/ru.po000066400000000000000000002326521520625676500202750ustar00rootroot00000000000000# Russian translation for focus-timer # Copyright (c) 2012-2026 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # liberumed , 2013 # Max Vetrov 'tigertv' , 2018 # Roman Kaverin , 2020 # ViktorOn , 2022-2026 # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-26 11:41+0200\n" "Last-Translator: ViktorOn \n" "Language-Team: Russian\n" "Language: ru\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "Таймер продуктивности, который помогает работать эффективнее, разделяя время " "на периоды, для сосредоточения на работе, и короткие перерывы. Работайте 25 " "минут, затем сделайте 5-минутный перерыв, чтобы сохранить концентрацию и " "избежать выгорания." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Основные возможности:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Настраиваемая длительность рабочих сессий и перерывов" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Экранная заставка во время перерывов" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Таймер" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Статистика за день" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Статистика за месяц" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Настройки" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Покрытие экрана" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "Обновлен русский перевод (спасибо @ViktorOn)" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "Старт или Стоп" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "Старт, Пауза или Возобновить" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Запустить Помодоро" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Старт" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Стоп" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Пауза" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Возобновить" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Пропустить" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Перемотать назад" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Продлить текущий помодоро или перерыв" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Сбросить" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "Показать настройки" #: src/application.vala:203 msgid "Quit application" msgstr "Выйти из приложения" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Вывести информацию о версии и выйти" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Переместить в фокус" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "Осталось %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "Пользовательское действие \"%s\" не выполнено" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Время ожидания истекло" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "Не удалось выполнить команду" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "Команда не указана" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Незакрытая кавычка" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Недопустимая команда" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Неизвестная переменная \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Неизвестный формат \"%s\"" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Программа \"%s\" не найдена" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Действия" #: src/core/event.vala:183 msgid "Countdown" msgstr "Обратный отсчет" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Сессия" #: src/core/event.vala:189 msgid "Other" msgstr "Другое" #: src/core/event.vala:269 msgid "Started the timer." msgstr "Таймер запущен." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "Таймер остановлен вручную." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Обратный отсчет был приостановлен вручную. Не срабатывает при блокировке " "экрана или при переходе в спящий режим." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Обратный отсчет был возобновлен вручную." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "Переход к следующему блоку времени до завершения отсчета." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "Использовано действие перемотки. Оно добавляет паузу в прошлом." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Сессия очищена вручную." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Завершено" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Обратный отсчет завершен. При ожидании подтверждения длительность временного " "блока еще может быть изменена." #: src/core/event.vala:333 msgid "Changed" msgstr "Изменено" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "Срабатывает при любом изменении обратного отсчета." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Подтверждение перехода" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "" "Требуется ручное подтверждение для запуска следующего временного блока." #: src/core/event.vala:350 msgid "Advanced" msgstr "Переход" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Совершен переход или пропуск к следующему временном блоку." #: src/core/event.vala:358 msgid "State Changed" msgstr "Состояние изменено" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Переход к следующему блоку или изменение типа перерыва." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Перенесено" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Срабатывает при изменении запланированных временных блоков." #: src/core/event.vala:374 msgid "Expired" msgstr "Истекло" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "Срабатывает, когда сессия настроена на сброс из-за бездействия." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Помодоро" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Сделать перерыв" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Сделать короткий перерыв" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Сделать длинный перерыв" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Помодоро скоро завершится" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "Сделать перерыв" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Перерыв скоро закончится" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 минута" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Приготовьтесь…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Помодоро завершен!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "Перерыв окончен!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Подтвердите запуск Помодоро…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Подтвердите начало перерыва…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Подтвердите начало короткого перерыва…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Подтвердите начало длинного перерыва…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Пропустить перерыв" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Не удалось инициализировать воспроизведение" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Файл не найден" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Тип файла не поддерживается" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "Остановлено" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Перерыв" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Короткий перерыв" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Длинный перерыв" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%u ч." #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%u мин." #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u час" msgstr[1] "%u часа" msgstr[2] "%u часов" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u минута" msgstr[1] "%u минуты" msgstr[2] "%u минут" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u секунда" msgstr[1] "%u секунды" msgstr[2] "%u секунд" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Точное время текущего события." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Текущая фаза цикла Помодоро. Возможные значения: stopped " "(остановка), pomodoro (помодоро), break (перерыв), " "short-break (короткий перерыв), long-break (длинный " "перерыв)." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Статус текущего блока времени. Возможные значения: scheduled " "(запланировано), in-progress (в процессе), completed " "(завершено), uncompleted (не завершено)." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "Флаг, указывающий на начало обратного отсчета." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "Флаг, указывающий, что отсчет стоит на паузе." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "Флаг, указывающий на завершение отсчета." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "Флаг, указывающий, что таймер активен." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Длительность текущего отсчета." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Разница между истекшим временем и реально прошедшим." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "Время, затраченное на обратный отсчет." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "Время, оставшееся до конца отсчета." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Время начала обратного отсчета." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "Расширение GNOME Shell" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Сделайте использование приложения еще удобнее!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "" "Включите расширение GNOME Shell для полной интеграции с рабочим столом" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Всегда под рукой" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "Управляйте таймером прямо из верхней панели, не открывая приложение" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Меньше отвлекающих факторов" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Улучшенные напоминания о перерывах" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "Элегантный полноэкранный оверлей сделает перерывы более приятными" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Готовы попробовать?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "_Установить расширение" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "_Не сейчас" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Что-то пошло не так" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Копировать в буфер обмена" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Попробовать снова" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "_Прервать" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Время ожидания истекло" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Установка расширений запрещена" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "Не удалось загрузить расширение" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Покрытие экрана (оверлей)" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "Рабочий стол" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "Доступно расширение GNOME Shell" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Узнать больше" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "Не используется" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Завершено!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Статистика" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Выход" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Журнал" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Журнал пуст" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Записи появятся здесь после запуска таймера." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Контекст" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Команда" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Вывод" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Ошибка" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Код выхода:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Время выполнения:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "liberumed , 2013\n" "Max 'tigertv' Vetrov , 2018\n" "Roman Kaverin , 2020\n" "Alexandre Prokoudine , 2021\n" "ViktorOn https://github.com/ViktorOn 2022" #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Пожертвовать" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "Перерывы" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Прерывания" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "Соотношение перерывов" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "День" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Неделя" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Месяц" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "Здесь пока ничего нет" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Завершите несколько помодоро, чтобы заполнить статистику!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Пропущен %u день" msgstr[1] "Пропущено %u дня" msgstr[2] "Пропущено %u дней" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Пропущена %u неделя" msgstr[1] "Пропущено %u недели" msgstr[2] "Пропущено %u недель" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Пропущен %u месяц" msgstr[1] "Пропущено %u месяца" msgstr[2] "Пропущено %u месяцев" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "Сегодня" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "Вчера" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "На этой неделе" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "Неделя %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Неделя %u из %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Помодоро" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Короткий перерыв" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Длинный перерыв" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_Перерыв" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Открыть полноэкранный оверлей" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Срок сессии истек" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "Длинный перерыв через %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "Вернуть на одну минуту назад" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_Компактный вид" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Настройки" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_О приложении" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_Выход" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Основное меню" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Оставить таймер запущенным?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Вы можете оставить его работать в фоне — уведомления и горячие клавиши " "продолжат работать." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "Работа в фоне" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Пора сделать перерыв" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "Главное окно" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Предпочитать темную тему" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Предпочитать компактный вид" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "Запущено" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "На паузе" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "Редактировать действие" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Отмена" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Сохранить" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Название" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "Триггер" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Событие" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Выполнить команду после события." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Условие" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "Выполнить вторую команду, когда условие перестанет соблюдаться." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "События" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Добавить _событие" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Фильтр" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Фильтр" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "Команда оболочки" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "Команды" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Команда при соблюдении условия" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Команда при несоблюдении условия" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Рабочий каталог" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Использовать подсеть (Subshell)" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Запускать программу через оболочку, например sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Передавать входные данные" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "Вместо переменных можно обрабатывать объект JSON." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Ожидать завершения" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Блокировать выполнение других команд до завершения текущей." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "_Удалить действие" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "События еще не указаны." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "Добавить действие" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Добавить" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Выберите рабочий каталог" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Выбрать" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "Действие без названия" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Добавить условие" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Добавить группу" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "И" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ИЛИ" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Является" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Не является" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Равно" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Больше чем" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Меньше чем" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Да" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Нет" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "Минуты" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Секунды" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Часы" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Выберите поле…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Состояние" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Запущено" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "Продолжительность" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Вставить переменную" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Формат" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Журнал" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Показать журнал выполнения" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Автоматически запускать команды оболочки при событиях или условиях таймера. " "Узнать больше." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "Установить комбинацию клавиш" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Установить" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Отключено" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Нажмите Esc для отмены или Backspace, чтобы отключить " "комбинацию" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Глобальные комбинации клавиш позволяют управлять приложением, даже если его " "нет на экране. Они работают, пока приложение запущено в фоне." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Открыть настройки для редактирования комбинаций клавиш" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "_Правка" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Введите новую комбинацию для запуска или остановки таймера" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Введите комбинацию для действий старт/пауза/возобновление" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Введите комбинацию для запуска таймера" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Введите комбинацию для остановки таймера" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Введите комбинацию для паузы таймера" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Введите комбинацию для возобновления таймера" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Введите комбинацию для пропуска" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Вернуть на минуту назад" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Введите комбинацию для перемотки назад" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Введите комбинацию для вывода окна в фокус" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Оповещения" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Время заканчивается" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "Уведомлять, когда Помодоро или перерыв подходят к концу." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "Полноэкранное уведомление, помогающее не пропускать перерыв." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Задержка блокировки" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Период бездействия перед блокировкой экрана." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Задержка повторного открытия" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "Период бездействия перед повторным открытием оверлея после закрытия." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Никогда" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Уведомления" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Звуки" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Внешний вид" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "Комбинации клавиш" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "Автоматизация" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Звуки отключены" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "Звуки оповещений" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Звук завершения помодоро" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "Звук завершения перерыва" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "Фоновый звук" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Колокольчик" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Громкий колокольчик" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Тиканье часов" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Нет" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Громкость:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Выберите звуковой файл" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Длительность Помодоро" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "Длительность короткого перерыва" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "Длительность длинного перерыва" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Количество циклов" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Поведение" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Пауза при блокировке экрана" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Подтверждать начало перерыва" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Подтверждать начало Помодоро" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "Одна сессия займет %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% времени будет отведено на перерывы." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Применить изменения к текущему Помодоро?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Применить изменения к текущему перерыву?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "Применить" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Боковая панель" #~ msgid "Time management utility" #~ msgstr "Утилита для управления временем" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Сохраняйте концентрацию, делая регулярные перерывы" #~ msgid "Visual and audio notifications" #~ msgstr "Визуальные и звуковые уведомления" #~ msgid "Time tracking and statistics" #~ msgstr "Учет времени и статистика" #~ msgid "GNOME desktop integration" #~ msgstr "Интеграция с рабочим столом GNOME" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Запуск команд после завершения Помодоро или перерыва" #~ msgid "Compact timer" #~ msgstr "Компактный таймер" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Обзор изменений в gnome-pomodoro 0.28.1" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "Добавлен перевод на тамильский (спасибо @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "Добавлен перевод на иврит (спасибо @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Обзор изменений в gnome-pomodoro 0.28.0" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Поддержка GNOME Shell 49 (спасибо @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Обновлен немецкий перевод (спасибо @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Обзор изменений в gnome-pomodoro 0.27.0" #~ msgid "Support for GNOME Shell 48" #~ msgstr "Поддержка GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Разделение времени, проведенного в полночь" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "Добавлен перевод на телугу (спасибо @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Обзор изменений в gnome-pomodoro 0.26.0" #~ msgid "Support for GNOME Shell 47" #~ msgstr "Поддержка GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "" #~ "Разрешить скрывать экранное наложение жестом во время воспроизведения " #~ "видео" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "Добавлен перевод на грузинский (спасибо @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Скорректированы переводы в appdata (спасибо @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Обзор изменений в gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Исправлено отображение уведомления после продления Помодоро" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Обзор изменений в gnome-pomodoro 0.25.1" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Исправления для GNOME Shell 46" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Прекращение поддержки GNOME Shell 45" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Обзор изменений в gnome-pomodoro 0.25.0" #~ msgid "Support for GNOME Shell 46" #~ msgstr "Поддержка GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Скрипт сборки адаптирован для meson 0.59.0 (спасибо @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Позвольте Pomodoro управлять системными уведомлениями во время " #~ "работы таймера" #~ msgid "15 seconds" #~ msgstr "15 секунд" #~ msgid "30 seconds" #~ msgstr "30 секунд" #~ msgid "1 minute" #~ msgstr "1 минута" #~ msgid "2 minutes" #~ msgstr "2 минуты" #~ msgid "3 minutes" #~ msgstr "3 минуты" #~ msgid "5 minutes" #~ msgstr "5 минут" #~ msgid "Timer Ticking" #~ msgstr "Тиканье таймера" #~ msgid "Birds" #~ msgstr "Птицы" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "таймер;timer;помодоро;pomodoro;" #~ msgid "Start/Stop" #~ msgstr "Старт/Стоп" #~ msgid "Pause/Resume" #~ msgstr "Пауза/Возобновить" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "Пропустить до помодоро или до перерыва" #~ msgid "Reset current session" #~ msgstr "Сбросить текущую сессию" #~ msgid "Run as background service" #~ msgstr "Запускать как фоновый процесс" #~ msgid "About Pomodoro" #~ msgstr "О Помодоро" #~ msgid "A simple time management utility" #~ msgstr "Простой инструмент для тайм-менеджмента" #~ msgid "_Stopped" #~ msgstr "_Остановлено" #~ msgid "Extension for GNOME Shell is available" #~ msgstr "Поддержка расширения для GNOME Shell" #~ msgid "_Install" #~ msgstr "_Установить" #~ msgid "Failed to install extension" #~ msgstr "Ошибка при установке расширения" #, c-format #~ msgid "Long break due in %s" #~ msgstr "Долгий перерыв через %s" #~ msgid "A time management utility for GNOME" #~ msgstr "Инструмент тайм-менеджмента для GNOME" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "Утилита GNOME для управления временем в соответствии с техникой Помодоро. " #~ "Она помогает повысить производительность и сосредоточенность с помощью " #~ "коротких перерывов после каждых 25 минут работы." #~ msgid "Timer window" #~ msgstr "Окно таймера" #~ msgid "Indicator for GNOME Shell" #~ msgstr "Индикатор" #~ msgid "Overview of changes in gnome-pomodoro 0.24.1" #~ msgstr "Изменения в gnome-pomodoro 0.24.1" #~ msgid "Overview of changes in gnome-pomodoro 0.24.0" #~ msgstr "Изменения в gnome-pomodoro 0.24.0" #~ msgid "Overview of changes in gnome-pomodoro 0.23.1" #~ msgstr "Изменения в gnome-pomodoro 0.23.1" #~ msgid "Overview of changes in gnome-pomodoro 0.23.0" #~ msgstr "Изменения в gnome-pomodoro 0.23.0" #~ msgid "Overview of changes in gnome-pomodoro 0.22.1" #~ msgstr "Изменения в gnome-pomodoro 0.22.1" #~ msgid "Overview of changes in gnome-pomodoro 0.22.0" #~ msgstr "Изменения в gnome-pomodoro 0.22.0" #~ msgid "Updated Brazilian translation (thanks @costaronaldo)" #~ msgstr "Обновлен бразильский перевод (спасибо @antoniofsm)" #~ msgid "Updated Chinese translation (thanks @HaorongX)" #~ msgstr "Обновлен китайский перевод (спасибо @HaorongX)" #~ msgid "Overview of changes in gnome-pomodoro 0.21.1" #~ msgstr "Изменения в gnome-pomodoro 0.21.1" #~ msgid "Overview of changes in gnome-pomodoro 0.21.0" #~ msgstr "Изменения в gnome-pomodoro 0.21.0" #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "Поддержка GNOME Shell 42 (@milotype и @kappa)" #~ msgid "Added Croatian translation (thanks @dayeondev)" #~ msgstr "Добавлен хорватский перевод (спасибо @dayeondev)" #~ msgid "Overview of changes in gnome-pomodoro 0.20.0" #~ msgstr "Изменения в gnome-pomodoro 0.20.0" #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "Поддержка GNOME Shell 41 (@mbooth101)" #~ msgid "Overview of changes in gnome-pomodoro 0.19.2" #~ msgstr "Изменения в gnome-pomodoro 0.19.2" #~ msgid "Updated Russian translation (@prokoudine)" #~ msgstr "Обновлен русский перевод (@prokoudine)" #~ msgid "Updated Dutch translation (@Vistaus)" #~ msgstr "Обновлен немецкий перевод (@Vistaus)" #~ msgid "Overview of changes in gnome-pomodoro 0.19.1" #~ msgstr "Изменения в gnome-pomodoro 0.19.1" #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "Поддержка GNOME Shell 40.0, не 4.0" #~ msgid "Overview of changes in gnome-pomodoro 0.19.0" #~ msgstr "Изменения в gnome-pomodoro 0.19.0" #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "Поддержка GNOME Shell 4.0" #~ msgid "Changed blur effect during break" #~ msgstr "Фоновое размытие экрана во время перерыва" #~ msgid "Added Korean translation (@dayeondev)" #~ msgstr "Добавлен корейский перевод (@dayeondev)" #~ msgid "Updated Brazilian translation (@alexandreafa)" #~ msgstr "Обновлен бразильский перевод (@alexandreafa)" #~ msgid "Overview of changes in gnome-pomodoro 0.18.0" #~ msgstr "Изменения в gnome-pomodoro 0.18.0" #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "Поддержка GNOME Shell 3.38 (@ignapk и @szpak)" #~ msgid "Added Norwegian translation (@arnotixe)" #~ msgstr "Добавлен норвежский перевод (@arnotixe)" #~ msgid "Added Finnish translation (@iqqmuT)" #~ msgstr "Добавлен финский перевод (@iqqmuT)" #~ msgid "Updated Indonesian translation (@atriwidada)" #~ msgstr "Обновлен индонезийский перевод (@atriwidada)" #~ msgid "Updated Chinese translation (@wffger)" #~ msgstr "Обновлен китайский перевод (@wffger)" #~ msgid "Updated Russian translation (@rkaverin)" #~ msgstr "Обновлен русский перевод (@rkaverin)" #~ msgid "Обновлен французский перевод (@precondition)" #~ msgstr "Обновлен перевод на каталанский (@antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.17.0" #~ msgstr "Изменения в gnome-pomodoro 0.17.0" #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "Поддержка GNOME Shell 3.36" #~ msgid "Updated Catalan translation (@antoniofsm)" #~ msgstr "Обновлен каталанский перевод (@antoniofsm)" #~ msgid "Overview of changes in gnome-pomodoro 0.16.0" #~ msgstr "Изменения в gnome-pomodoro 0.16.0" #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "Поддержка GNOME Shell 3.34" #~ msgid "Added esperanto translation (@SeZuo)" #~ msgstr "Добавлен перевод на эсперанто (@SeZuo)" #~ msgid "Moved app-menu to main window" #~ msgstr "Меню приложения перенесено в главное окно" #~ msgid "Overview of changes in gnome-pomodoro 0.15.1" #~ msgstr "Изменения в gnome-pomodoro 0.15.1" #~ msgid "Minor code cleanups" #~ msgstr "Минорные правки кода" #~ msgid "Overview of changes in gnome-pomodoro 0.15.0" #~ msgstr "Изменения в gnome-pomodoro 0.15.0" #~ msgid "Minor code cleanups to support ES6 syntax" #~ msgstr "Минорные правки кода для поддержки синтаксиса ES6" #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "Поддержка GNOME Shell 3.32 (@demokritos)" #~ msgid "Fix for build with vala 0.44.1 (@snizovtsev)" #~ msgstr "Фикс для компиляции на vala 0.44.1 (@snizovtsev)" #~ msgid "Updated German translation (@c7hm4r)" #~ msgstr "Обновлен перевод на немецкий (@c7hm4r)" #~ msgid "Fix for handle error recreating existing folder (@Rj7)" #~ msgstr "" #~ "Исправлена обработка ошибки при пересоздании существующей папки (@Rj7)" #~ msgid "Overview of changes in gnome-pomodoro 0.14.0" #~ msgstr "Изменения в gnome-pomodoro 0.14.0" #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "Поддержка GNOME Shell 3.28 и 3.30 (@aerostitch)" #~ msgid "Background blur under the dialog during breaks" #~ msgstr "Фоновое размытие экрана во время перерыва" #~ msgid "Updated German translation (@tsabsch)" #~ msgstr "Обновлен перевод на немецкий (@tsabsch)" #~ msgid "Updated Russian translation (@tigertv)" #~ msgstr "Обновлен русский перевод (@tigertv)" #~ msgid "_Timer" #~ msgstr "_Таймер" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "Комбинации клавиш для переключения таймера. Укажите новую комбинацию." #~ msgid "Pomodoros before a long break" #~ msgstr "Количество помодоро до длинного перерыва" #~ msgid "Keyboard shortcut" #~ msgstr "Комбинация клавиш" #~ msgid "Screen notifications" #~ msgstr "Экранные уведомления" #~ msgid "Wait for activity after a break" #~ msgstr "Дождаться активности пользователя после перерыва" #~ msgid "Plugins…" #~ msgstr "Плагины..." #~ msgid "Plugins" #~ msgstr "Плагины" #~ msgid "Back" #~ msgstr "Назад" #~ msgid "Complete a few sessions" #~ msgstr "Нет завершенных сессий" #~ msgid "Previous (Alt+Left)" #~ msgstr "Предыдущий (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "Следующий (Alt+Right)" #~ msgid "Complete" #~ msgstr "Завершить" #~ msgid "Enable" #~ msgstr "Включить" #~ msgid "Add" #~ msgstr "Добавить" #~ msgid "Remove" #~ msgstr "Удалить" #~ msgid "Elapsed Time" #~ msgstr "Прошло" #~ msgid "Pause Timer" #~ msgstr "Таймер на паузу" #~ msgid "Pause break" #~ msgstr "Пауза" #~ msgid "Pause Pomodoro" #~ msgstr "Пауза" #~ msgid "Resume break" #~ msgstr "Возобновить" #~ msgid "Resume Pomodoro" #~ msgstr "Возобновить" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "Осталась %d минута" #~ msgstr[1] "Осталось %d минуты" #~ msgstr[2] "Осталось %d минут" #~ msgid "Report issue" #~ msgstr "Сообщить о проблеме" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "Невозможно запустить сервис %s" #~ msgid "Woodland Birds" #~ msgstr "Лесные птицы" #~ msgid "End of Break Sound" #~ msgstr "Звук окончания перерыва" #~ msgid "Start of Break Sound" #~ msgstr "Звук начала перерыва" #~ msgid "Off" #~ msgstr "Выключить" #~ msgid "Ticking sound" #~ msgstr "Звук тиканья" #~ msgid "Start of break sound" #~ msgstr "Звук начала перерыва" #~ msgid "End of break sound" #~ msgstr "Звук окончания перерыва" #~ msgid "Focus on your task." #~ msgstr "Сосредоточьтесь на Вашей задаче." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "У вас осталась %d минута" #~ msgstr[1] "У вас осталось %d минуты" #~ msgstr[2] "У вас осталось %d минут" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "У вас осталась %d секунда" #~ msgstr[1] "У вас осталось %d секунды" #~ msgstr[2] "У вас осталось %d секунд" #~ msgid "Take a longer break" #~ msgstr "Длинный перерыв" #~ msgid "Lengthen it" #~ msgstr "Удлинить перерыв" #~ msgid "Shorten it" #~ msgstr "Укоротить перерыв" #~ msgid "Start pomodoro" #~ msgstr "Начать новый помодоро" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "Использование \"%s\" не подходит в качестве комбинации клавиш. Попробуйте " #~ "добавить в комбинацию другие клавиши: Ctrl, Alt или Shift." #~ msgid "Available" #~ msgstr "Доступен" #~ msgid "Busy" #~ msgstr "Занят" #~ msgid "Idle" #~ msgstr "Отсутствует" #~ msgid "Invisible" #~ msgstr "Невидимый" #, c-format #~ msgid "%d m" #~ msgstr "%d м" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f ч" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f ч" #~ msgid "Extension is out of date" #~ msgstr "Расширение устарело" #~ msgid "Upgrade" #~ msgstr "Обновить" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "_Статистика" #~ msgid "It seems to be uninstalled" #~ msgstr "Расширение удалено" #~ msgid "A new pomodoro is starting" #~ msgstr "Начало нового помодоро!" #~ msgid "Hey, you're missing out on a break" #~ msgstr "Эй, вы пропускаете перерыв" #~ msgid "Could not run pomodoro" #~ msgstr "Не удалось запустить Помодоро" #~ msgid "Looks like gnome-pomodoro is not installed" #~ msgstr "Похоже, gnome-pomodoro не установлен" #~ msgid "Remind to take a break" #~ msgstr "Напоминания для перерывов" #~ msgid "Select sound for pomodoro start" #~ msgstr "Выберите звук для проигрывания при старте помодоро" #~ msgid "Presence" #~ msgstr "Наличие" #~ msgid "Status during pomodoro" #~ msgstr "Статус во время помодоро" #~ msgid "" #~ "System notifications including chat messages won't show up during " #~ "pomodoro." #~ msgstr "" #~ "Изменить онлайн статус и отключить уведомления, включая сообщения чата" #~ msgid "" #~ "System notifications including chat messages won't show up during break." #~ msgstr "" #~ "Системные уведомления, включая сообщения чата, не будут показываться в " #~ "течении перерыва." #~ msgid "System notifications including chat messages won't show up." #~ msgstr "" #~ "Системные уведомления, включая сообщения чата, не будут показываться." #~ msgid "" #~ "The shortcut \"%s\" cannot be used because it will become impossible to " #~ "type using this key.\n" #~ "Please try with a key such as Control, Alt or Shift at the same time." #~ msgstr "" #~ "Сочетание клавиш \"%s\" не может быть использовано потому что невозможно " #~ "напечатать используя эту клавишу.\n" #~ "Пожалуйста, попробуйте клавиши: CTRL, ALT или SHIFT при нажатии." #~ msgid "_No sound" #~ msgstr "_Без звука" #~ msgid "_Open" #~ msgstr "_Открыть" #~ msgid "All files" #~ msgstr "Все файлы" #~ msgid "Supported audio files" #~ msgstr "Поддерживаются аудио файлы" #~ msgid "Manage your time and tasks" #~ msgstr "Управление временем и задачами" #~ msgid "Reset Counts and Timer" #~ msgstr "Сбросить Счётчик и Таймер" #~ msgid "Click to reset session counts to zero" #~ msgstr "Нажмите, чтобы сбросить счётчик на ноль" #~ msgid "Away From Desk" #~ msgstr "Удаленный Режим" #~ msgid "Set optimal settings for doing paperwork" #~ msgstr "Настройки для работы с бумагами" #~ msgid "Control Presence Status" #~ msgstr "Управление Статусом Присутствия" #~ msgid "Show Dialog Messages" #~ msgstr "Показывать Уведомления" #~ msgid "Show a dialog message at the end of pomodoro session" #~ msgstr "Показывать уведомления в конце помодоро сессии" #~ msgid "Play a sound at start of pomodoro session" #~ msgstr "Проигрывать звук в начале сессии помодоро" #~ msgid "%d Completed Session" #~ msgid_plural "%d Completed Sessions" #~ msgstr[0] "%d завершенная сессия" #~ msgstr[1] "%d завершенные сессии" #~ msgstr[2] "%d завершенных сессий" #~ msgid "Hide" #~ msgstr "Скрыть" #~ msgid "Timer toggle key" #~ msgstr "Кнопка запуска таймера" #~ msgid "Time in seconds you are supposed to be working." #~ msgstr "Время в секундах отведенное для работы" #~ msgid "Time in seconds you are supposed to have a short break." #~ msgstr "Время в секундах отведенное для короткого перерыва" #~ msgid "Long pause duration" #~ msgstr "Длительность длинной паузы" #~ msgid "Time in seconds you are supposed to have a longer break." #~ msgstr "Время в секундах, отведенное для большого перерыва" #~ msgid "Whether to show a notification dialog when pause starts." #~ msgstr "Следует ли показывать диалоговые сообщения во время паузы." #~ msgid "Disable flexible breaks" #~ msgstr "Отключить гибкие перерывы" #~ msgid "Whether you are not using a computer to work." #~ msgstr "Если компьютер не используется во время работы." #~ msgid "Change user presence status to busy" #~ msgstr "Изменить статус присутствия на занят" #~ msgid "Whether to change user and IM presence to busy." #~ msgstr "Следует ли изменять статус присутствия пользователя и IM на занят." #~ msgid "Whether to play a sound to notify of events." #~ msgstr "Следует ли проигрывать звук для уведомлений и событий." #~ msgid "Notification sound file" #~ msgstr "Файл звукового уведоления" #~ msgid "Restore timer state" #~ msgstr "Востанавливать состояние таймера" #~ msgid "Whether to restore state on startup." #~ msgstr "Следует ли востанавливать состояние при загрузке." #~ msgid "Number of completed sessions since long break" #~ msgstr "Количество завершенных сессий после длительного перерыва." #~ msgid "Saved timer state" #~ msgstr "Сохраненное состояние таймера" #~ msgid "Time of saved state" #~ msgstr "Время сохраненного состояния" focustimerhq-FocusTimer-8581be2/po/sv.po000066400000000000000000001473431520625676500203010ustar00rootroot00000000000000# Swedish translation for focus-timer # Copyright © 2017, 2025, 2026 Free Software Foundation, Inc. # This file is distributed under the same license as the focus-timer package. # Anders Jonsson , 2017, 2025, 2026. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 09:26+0200\n" "PO-Revision-Date: 2026-01-13 00:53+0100\n" "Last-Translator: Anders Jonsson \n" "Language-Team: Swedish \n" "Language: sv\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.8\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "Focus Timer" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "En produktivitetstidtagare som hjälper dig arbeta mer effektivt genom att " "dela upp din tid i fokuserade arbetssessioner följda av korta raster. Arbeta " "25 minuter, ta sedan en fem minuters rast för att behålla " "koncentrationsförmågan och undvika att bränna ut dig." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 msgid "Key features:" msgstr "Huvudsakliga funktioner:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 msgid "Customizable work session and break lengths" msgstr "Anpassningsbara längder på arbetssessioner och rastlängder" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 msgid "Screen overlay during breaks" msgstr "Skärmöverlägg under raster" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "Tidtagare" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 msgid "Daily stats" msgstr "Dagsstatistik" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 msgid "Monthly stats" msgstr "Månadsstatistik" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "Inställningar" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 msgid "Screen overlay" msgstr "Skärmöverlägg" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 msgid "Start or Stop" msgstr "Starta eller stoppa" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 msgid "Start, Pause or Resume" msgstr "Starta, pausa eller återuppta" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "Starta Pomodoro" #: src/application.vala:164 msgid "Start break" msgstr "Starta rast" #: src/application.vala:167 msgid "Start short break" msgstr "Starta kort rast" #: src/application.vala:170 msgid "Start long break" msgstr "Starta lång rast" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "Starta" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "Stoppa" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "Pausa" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "Återuppta" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "Hoppa över" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 msgid "Rewind" msgstr "Spola tillbaka" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "SEKUNDER" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "Förläng nuvarande pomodoro eller rast" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "Återställ" #: src/application.vala:197 msgid "Print timer status" msgstr "Skriv ut tidtagarstatus" #: src/application.vala:200 msgid "Show preferences" msgstr "Visa inställningar" #: src/application.vala:203 msgid "Quit application" msgstr "Avsluta program" #: src/application.vala:206 msgid "Print version information and exit" msgstr "Skriv ut versionsinformation och avsluta" #: src/application.vala:240 msgid "Timer Options:" msgstr "Tidtagaralternativ:" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "Visa alternativ för att styra tidtagaren" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "Fel kan rapporteras på: %s" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 msgid "Bring to Focus" msgstr "Ge fokus" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, c-format msgid "%s remaining" msgstr "%s återstår" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" "Ogiltig användning. Skicka en flagga för att styra tidtagaren åt gången." #: src/core/action-manager.vala:113 #, c-format msgid "Custom action \"%s\" has failed" msgstr "Anpassade åtgärd ”%s” har misslyckats" #: src/core/command.vala:379 msgid "Reached timeout" msgstr "Tidsgräns uppnådd" #: src/core/command.vala:408 msgid "Failed to execute command" msgstr "Misslyckades med att köra kommando" #: src/core/command.vala:491 src/core/command.vala:506 msgid "Command is empty" msgstr "Kommandot är tomt" #: src/core/command.vala:510 msgid "Unclosed quotation mark" msgstr "Oavslutat citattecken" #: src/core/command.vala:515 msgid "Invalid command" msgstr "Ogiltigt kommando" #: src/core/command.vala:540 src/core/expression.vala:859 #, c-format msgid "Unknown variable \"%s\"" msgstr "Okänd variabel ”%s”" #: src/core/command.vala:546 src/core/expression.vala:236 #, c-format msgid "Unknown format \"%s\"" msgstr "Okänt format ”%s”" #: src/core/command.vala:619 #, c-format msgid "Program \"%s\" not found" msgstr "Programmet ”%s” hittades inte" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "Åtgärder" #: src/core/event.vala:183 msgid "Countdown" msgstr "Nedräkning" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "Session" #: src/core/event.vala:189 msgid "Other" msgstr "Annat" #: src/core/event.vala:269 msgid "Started the timer." msgstr "Startade tidtagaren." #: src/core/event.vala:277 msgid "Stopped the timer manually." msgstr "Stoppade tidtagaren manuellt." #: src/core/event.vala:285 msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "Nedräkningen har pausats manuellt. Utlöses inte när skärmen låses eller när " "systemet försätts i vänteläge." #: src/core/event.vala:293 msgid "The countdown has been manually resumed." msgstr "Nedräkningen har manuellt återupptagits." #: src/core/event.vala:301 msgid "Jumped to a next time-block before the countdown has finished." msgstr "Hoppade till ett nästa tidsblock innan nedräkningen hade slutförts." #: src/core/event.vala:309 msgid "Rewind action has been used. It adds a pause in the past." msgstr "" "Spola tillbaka-åtgärden har används. Den lägger till en paus i det förflutna." #: src/core/event.vala:317 msgid "Manually cleared the session." msgstr "Tömde manuellt sessionen." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "Färdig" #: src/core/event.vala:326 msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "Nedräkningen har slutförts. Om den väntar på bekräftelse kan längden på " "tidsblocket fortfarande ändras." #: src/core/event.vala:333 msgid "Changed" msgstr "Ändrad" #: src/core/event.vala:334 msgid "Triggered on any change related to the countdown." msgstr "Utlöses vid alla ändringar relaterade till nedräkningen." #. Session #: src/core/event.vala:342 msgid "Confirm Advancement" msgstr "Bekräfta avancemang" #: src/core/event.vala:343 msgid "A manual confirmation is required to start next time-block." msgstr "Manuell bekräftelse krävs för att starta nästa tidsblock." #: src/core/event.vala:350 msgid "Advanced" msgstr "Avancerat" #: src/core/event.vala:351 msgid "Transitioned or skipped to a next time-block." msgstr "Övergick eller hoppade till ett nytt tidsblock." #: src/core/event.vala:358 msgid "State Changed" msgstr "Tillstånd ändrat" #: src/core/event.vala:359 msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "Övergick till ett nästa tidsblock eller när en rast får en ny etikett." #: src/core/event.vala:366 msgid "Rescheduled" msgstr "Schemaändrad" #. translators: Change of plan #: src/core/event.vala:367 msgid "Triggered when scheduled time-blocks have changed." msgstr "Utlöses när schemalagda tidsblock har ändrats." #: src/core/event.vala:374 msgid "Expired" msgstr "Utgången" #: src/core/event.vala:375 msgid "Triggered when session is about to be reset due to inactivity." msgstr "" "Utlöses när sessionen håller på att återställas på grund av inaktivitet." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "Pomodoro" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "Ta en rast" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "Ta en kort paus" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "Ta en lång paus" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "Pomodoro håller på att ta slut" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 msgid "Take a Break" msgstr "Ta en rast" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "Rast håller på att ta slut" #: src/core/notification-manager.vala:436 msgid "+1 minute" msgstr "+1 minut" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "Gör dig redo…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 msgid "Pomodoro is over!" msgstr "Pomodoro är slut!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 msgid "Break is over!" msgstr "Rasten är över!" #: src/core/notification-manager.vala:518 msgid "Confirm the start of a Pomodoro…" msgstr "Bekräfta starten på en pomodoro…" #: src/core/notification-manager.vala:523 msgid "Confirm the start of a break…" msgstr "Bekräfta starten på en rast…" #: src/core/notification-manager.vala:528 msgid "Confirm the start of a short break…" msgstr "Bekräfta starten på en kort rast…" #: src/core/notification-manager.vala:533 msgid "Confirm the start of a long break…" msgstr "Bekräfta starten på en lång rast…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "Hoppa över rast" #: src/core/sound-player.vala:101 msgid "Failed to initialize playback" msgstr "Misslyckades med att initiera uppspelning" #: src/core/sounds.vala:112 msgid "File not found" msgstr "Filen hittades inte" #: src/core/sounds.vala:116 msgid "File type not supported" msgstr "Filtypen stöds inte" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 msgid "Stopped" msgstr "Stoppad" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "Rast" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "Kort rast" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "Lång rast" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%uh" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%um" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u timme" msgstr[1] "%u timmar" #: src/core/utils.vala:81 #, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u minut" msgstr[1] "%u minuter" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u sekund" msgstr[1] "%u sekunder" #: src/core/variables.vala:116 msgid "The exact time of the current event." msgstr "Den exakta tiden för aktuell händelse." #: src/core/variables.vala:121 msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "Aktuell fas för pomodorocykeln. Möjliga värden: stopped, " "pomodoro, break, short-break, long-break." #: src/core/variables.vala:126 msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "Status för aktuellt tidsblock. Möjliga värden: scheduled, in-" "progress, completed, uncompleted." #: src/core/variables.vala:131 msgid "A flag indicating whether countdown has begun." msgstr "En flagga som indikerar om nedräkningen har börjat." #: src/core/variables.vala:136 msgid "A flag indicating whether countdown is paused." msgstr "En flagga som indikerar om nedräkningen har pausats." #: src/core/variables.vala:141 msgid "A flag indicating whether countdown has finished." msgstr "En flagga som indikerar om nedräkningen har slutat." #: src/core/variables.vala:146 msgid "A flag indicating whether the timer is actively counting down." msgstr "En flagga som indikerar om tidtagaren håller på att räkna nedåt." #: src/core/variables.vala:151 msgid "Duration of the current countdown." msgstr "Längden på aktuell nedräkning." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 msgid "Discrepancy between elapsed time and the time passed." msgstr "Skillnaden mellan uppmätt tid och tiden som gått." #. translators: Time since the start of countdown #: src/core/variables.vala:163 msgid "The amount of time spent on the countdown." msgstr "Tiden som nedräkningen pågått." #. translators: Displayed timer value. #: src/core/variables.vala:169 msgid "The amount of time left before the countdown ends." msgstr "Tiden kvar innan nedräkningen tar slut." #: src/core/variables.vala:174 msgid "Time when the countdown has started." msgstr "Tiden när nedräkningen startades." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 msgid "GNOME Shell Extension" msgstr "GNOME Shell-tillägg" #: src/plugins/gnome/install-extension-dialog.ui:57 msgid "Get the best experience!" msgstr "Få den bästa upplevelsen!" #: src/plugins/gnome/install-extension-dialog.ui:68 msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "Aktivera GNOME Shell-tillägget för sömlös skrivbordsintegration" #: src/plugins/gnome/install-extension-dialog.ui:95 msgid "Always within reach" msgstr "Alltid inom räckhåll" #: src/plugins/gnome/install-extension-dialog.ui:106 msgid "Control timer directly from the top bar without opening the app" msgstr "Styr tidtagaren direkt från systemraden utan att öppna programmet" #: src/plugins/gnome/install-extension-dialog.ui:132 msgid "Less distractions" msgstr "Färre distraktioner" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 msgid "Refined break reminders" msgstr "Eleganta rastpåminnelser" #: src/plugins/gnome/install-extension-dialog.ui:181 msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "" "Elegant helskärmsöverlägg som gör det till en mer angenäm upplevelse att ta " "rast" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 msgid "Ready to try it?" msgstr "Redo att prova?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 msgid "_Install Extension" msgstr "_Installera tillägg" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 msgid "_Not Now" msgstr "I_nte nu" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 msgid "Something went wrong" msgstr "Något gick fel" #: src/plugins/gnome/install-extension-dialog.ui:364 msgid "Copy to clipboard" msgstr "Kopiera till urklipp" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 msgid "_Try Again" msgstr "_Försök igen" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 msgid "_Abort" msgstr "A_vbryt" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 msgid "Time-out reached" msgstr "Tidsgräns uppnådd" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 msgid "Installing extensions is not allowed" msgstr "Installation av tillägg är inte tillåtet" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 msgid "Failed to download the extension" msgstr "Misslyckades med att hämta tillägget" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 msgid "Screen Overlay" msgstr "Skärmöverlägg" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 msgid "GNOME Shell extension available" msgstr "GNOME Shell-tillägg tillgängligt" #: src/plugins/gnome/window-extension.vala:33 msgid "Learn More" msgstr "Läs mer" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 msgid "Unused" msgstr "Oanvänd" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 msgid "Finished!" msgstr "Färdig!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "Statistik" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "Avsluta" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 msgid "Log" msgstr "Logg" #: src/ui/log/log-window.ui:37 msgid "Empty Log" msgstr "Tom logg" #: src/ui/log/log-window.ui:38 msgid "Entries will show up here once you start the timer." msgstr "Poster kommer visas här då du startar tidtagaren." #: src/ui/log/log-window.ui:164 msgid "Context" msgstr "Sammanhang" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "Kommando" #: src/ui/log/log-window.ui:213 msgid "Output" msgstr "Utmatning" #: src/ui/log/log-window.ui:237 msgid "Error" msgstr "Fel" #: src/ui/log/log-window.ui:266 msgid "Exit Code:" msgstr "Avslutningskod:" #: src/ui/log/log-window.ui:277 msgid "Execution Time:" msgstr "Körningstid:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Anders Jonsson " #: src/ui/main/dialogs/about-dialog.vala:36 msgid "Donate" msgstr "Donera" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 msgid "Breaks" msgstr "Raster" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 msgid "Interruptions" msgstr "Avbrott" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 msgid "Break Ratio" msgstr "Rastförhållande" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "Dag" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "Vecka" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "Månad" #: src/ui/main/stats/stats-view.ui:39 msgid "Nothing to see here yet" msgstr "Inget att se här ännu" #: src/ui/main/stats/stats-view.ui:40 msgid "Finish a few Pomodoros to fill this up!" msgstr "Slutför några pomodoron för att fylla detta!" #: src/ui/main/stats/stats-view.vala:831 #, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "Hoppade över %u dag" msgstr[1] "Hoppade över %u dagar" #: src/ui/main/stats/stats-view.vala:837 #, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "Hoppade över %u vecka" msgstr[1] "Hoppade över %u veckor" #: src/ui/main/stats/stats-view.vala:843 #, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "Hoppade över %u månad" msgstr[1] "Hoppade över %u månader" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "I dag" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "I går" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "Denna vecka" #: src/ui/main/stats/stats-view.vala:1087 #, c-format msgid "Week %u" msgstr "Vecka %u" #: src/ui/main/stats/stats-view.vala:1088 #, c-format msgid "Week %u of %u" msgstr "Vecka %u av %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_Pomodoro" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_Kort rast" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_Lång rast" #: src/ui/main/timer/menus.ui:26 msgid "_Break" msgstr "_Rast" #: src/ui/main/timer/timer-view.ui:23 msgid "Open screen overlay" msgstr "Öppna skärmöverlägg" #: src/ui/main/timer/timer-view.vala:257 msgid "Session has expired" msgstr "Sessionen har gått ut" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, c-format msgid "Long break due in %s" msgstr "Lång rast om %s" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 msgid "Rewind one minute" msgstr "Spola tillbaka en minut" #: src/ui/main/window.ui:8 msgid "_Compact View" msgstr "_Kompakt vy" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_Inställningar" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_Om" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "A_vsluta" #: src/ui/main/window.ui:62 msgid "Primary Menu" msgstr "Primär meny" #: src/ui/main/window.vala:279 msgid "Keep timer running?" msgstr "Håll igång tidtagaren?" #: src/ui/main/window.vala:280 msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "Du kan låta den köra i bakgrunden — aviseringar och tangentbordsgenvägare " "kommer fortfarande fungera." #: src/ui/main/window.vala:287 msgid "Run in background" msgstr "Kör i bakgrunden" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "Det är dags för en rast" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 msgid "Main Window" msgstr "Huvudfönster" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 msgid "Prefer Dark Theme" msgstr "Föredra mörkt schema" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 msgid "Prefer Compact View" msgstr "Föredra kompakt vy" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 msgid "Started" msgstr "Startad" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "Pausad" #: src/ui/preferences/automation/action/action-edit-window.ui:26 msgid "Edit Custom Action" msgstr "Redigera anpassad åtgärd" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_Avbryt" #: src/ui/preferences/automation/action/action-edit-window.ui:46 msgid "_Save" msgstr "_Spara" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "Namn" #: src/ui/preferences/automation/action/action-edit-window.ui:76 msgid "Trigger" msgstr "Utlösare" #: src/ui/preferences/automation/action/action-edit-window.ui:80 msgid "Event" msgstr "Händelse" #: src/ui/preferences/automation/action/action-edit-window.ui:81 msgid "Execute command after an event." msgstr "Kör kommando efter en händelse." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 msgid "Condition" msgstr "Villkor" #: src/ui/preferences/automation/action/action-edit-window.ui:97 msgid "Ensure execution of a second command once condition is no longer met." msgstr "" "Säkerställ körning av ett andra kommando då villkoret inte längre uppfylls." #: src/ui/preferences/automation/action/action-edit-window.ui:114 msgid "Events" msgstr "Händelser" #: src/ui/preferences/automation/action/action-edit-window.ui:125 msgid "Add _Event" msgstr "Lägg till _händelse" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 msgid "_Filter" msgstr "_Filtrera" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 msgid "Filter" msgstr "Filter" #: src/ui/preferences/automation/action/action-edit-window.ui:191 msgid "Shell Command" msgstr "Skalkommando" #: src/ui/preferences/automation/action/action-edit-window.ui:199 msgid "Commands" msgstr "Kommandon" #: src/ui/preferences/automation/action/action-edit-window.ui:204 msgid "Condition Met Command" msgstr "Kommando för uppfyllt villkor" #: src/ui/preferences/automation/action/action-edit-window.ui:210 msgid "Condition Not Met Command" msgstr "Kommando för ej uppfyllt villkor" #: src/ui/preferences/automation/action/action-edit-window.ui:221 msgid "Working Directory" msgstr "Arbetskatalog" #: src/ui/preferences/automation/action/action-edit-window.ui:236 msgid "Use Subshell" msgstr "Använd underskal" #: src/ui/preferences/automation/action/action-edit-window.ui:237 msgid "Run the program from a subshell such as sh -c ''" msgstr "Kör programmet från ett underskal som sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 msgid "Pass Input Data" msgstr "Skicka inmatningsdata" #: src/ui/preferences/automation/action/action-edit-window.ui:243 msgid "Instead of passing variables you can process a JSON object." msgstr "I stället för att skicka variabler kan du bearbeta ett JSON-objekt." #: src/ui/preferences/automation/action/action-edit-window.ui:248 msgid "Wait For Completion" msgstr "Vänta på slutförande" #: src/ui/preferences/automation/action/action-edit-window.ui:249 msgid "Block execution of other commands until the command completes." msgstr "Blockera körning av andra kommandon till kommandot slutförs." #: src/ui/preferences/automation/action/action-edit-window.ui:259 msgid "_Delete Action" msgstr "_Ta bort åtgärd" #: src/ui/preferences/automation/action/action-edit-window.vala:230 msgid "No events specified yet." msgstr "Inga händelser angivna ännu." #: src/ui/preferences/automation/action/action-edit-window.vala:248 msgid "Add Custom Action" msgstr "Lägg till anpassad åtgärd" #: src/ui/preferences/automation/action/action-edit-window.vala:249 msgid "_Add" msgstr "_Lägg till" #: src/ui/preferences/automation/action/action-edit-window.vala:438 msgid "Select Working Directory" msgstr "Välj arbetskatalog" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_Välj" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 msgid "Untitled action" msgstr "Namnlös åtgärd" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 msgid "Add Condition" msgstr "Lägg till villkor" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 msgid "Add Group" msgstr "Lägg till grupp" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "OCH" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "ELLER" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Är" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Är inte" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Lika med" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Större än" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Mindre än" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "Ja" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "Nej" #: src/ui/preferences/automation/action/condition-widget.ui:95 msgid "Minutes" msgstr "Minuter" #: src/ui/preferences/automation/action/condition-widget.ui:96 msgid "Seconds" msgstr "Sekunder" #: src/ui/preferences/automation/action/condition-widget.ui:97 msgid "Hours" msgstr "Timmar" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 msgid "Select Field…" msgstr "Välj fält…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "Tillstånd" #: src/ui/preferences/automation/action/condition-widget.vala:119 msgid "Running" msgstr "Kör" #: src/ui/preferences/automation/action/condition-widget.vala:121 msgid "Duration" msgstr "Längd" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 msgid "Insert Variable" msgstr "Infoga variabel" #: src/ui/preferences/automation/action/variable-popover.ui:132 msgid "Format" msgstr "Format" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 msgid "_Log" msgstr "_Logg" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 msgid "Show execution log" msgstr "Visa körningslogg" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "Kör skalkommandon automatiskt vid tidtagarhändelser eller villkor. Läs mer." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 msgid "Set Shortcut" msgstr "Ställ in kortkommando" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 msgid "_Set" msgstr "_Ställ in" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 msgid "Disabled" msgstr "Inaktiverad" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "Tryck Esc för att avbryta eller Backsteg för att inaktivera " "tangentbordsgenvägen" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "Globala kortkommandon låter dig styra programmet även när det inte syns på " "skärmen. De fungerar så länge som programmet körs i bakgrunden." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 msgid "Open app settings for editing global shortcuts" msgstr "Öppna programinställningar för att redigera globala kortkommandon" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 msgid "_Edit" msgstr "R_edigera" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 msgid "Enter new shortcut for starting or stopping the timer" msgstr "Ange ett nytt kortkommando för att starta eller stoppa tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 msgid "Enter new shortcut to start/pause/resume the timer" msgstr "Ange ett nytt kortkommando för att starta/pausa/återuppta tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 msgid "Enter new shortcut for starting the timer" msgstr "Ange ett nytt kortkommando för att starta tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 msgid "Enter new shortcut for stopping the timer" msgstr "Ange ett nytt kortkommando för att stoppa tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 msgid "Enter new shortcut for pausing the timer" msgstr "Ange ett nytt kortkommando för att pausa tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 msgid "Enter new shortcut for resuming the timer" msgstr "Ange ett nytt kortkommando för att återuppta tidtagaren" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 msgid "Enter new shortcut for skipping" msgstr "Ange ett nytt kortkommando för att hoppa över" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 msgid "Rewind One Minute" msgstr "Spola tillbaka en minut" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 msgid "Enter new shortcut for rewinding" msgstr "Ange ett nytt kortkommando för att spola tillbaka" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 msgid "Enter new shortcut for bringing window to focus" msgstr "Ange ett nytt kortkommando för att ge fokus till fönstret" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 msgid "Announcements" msgstr "Aviseringar" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 msgid "Time Running Out" msgstr "Tiden håller på att ta slut" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 msgid "Notify when Pomodoro or break is about to end." msgstr "Avisera när pomodoro eller rast håller på att ta slut." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 msgid "A full-screen notification intended to enforce taking a break." msgstr "" "En helskärmsavisering som är avsedd för att tvinga fram att raster tas." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 msgid "Lock Delay" msgstr "Låsfördröjning" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 msgid "Period of inactivity to lock the screen." msgstr "Inaktivitetsperiod innan skärmen kommer att låsas." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 msgid "Reopen Delay" msgstr "Återöppningsfördröjning" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "" "Inaktivitetsperiod innan överlägget ska öppnas igen efter att det avvisats." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 msgid "Never" msgstr "Aldrig" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "Aviseringar" #: src/ui/preferences/preferences-window.vala:44 msgid "Sounds" msgstr "Ljud" #: src/ui/preferences/preferences-window.vala:51 msgid "Appearance" msgstr "Utseende" #: src/ui/preferences/preferences-window.vala:58 msgid "Keyboard Shortcuts" msgstr "Tangentbordsgenvägar" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 msgid "Automation" msgstr "Automatisering" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 msgid "Sounds Are Disabled" msgstr "Ljud är inaktiverade" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 msgid "Alert Sounds" msgstr "Larmljud" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 msgid "Pomodoro Finished Sound" msgstr "Ljud för slut på pomodoro" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 msgid "Break Finished Sound" msgstr "Ljud för slut på rast" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 msgid "Background Sound" msgstr "Bakgrundsljud" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "Ringklocka" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "Högljudd ringklocka" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "Tickande klocka" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 msgid "None" msgstr "Inget" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "Volym:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "Välj anpassat ljud" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 msgid "Pomodoro Duration" msgstr "Pomodorolängd" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 msgid "Short Break Duration" msgstr "Längd på kort rast" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 msgid "Long Break Duration" msgstr "Längd på lång rast" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 msgid "Number of Cycles" msgstr "Antal cykler" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 msgid "Behavior" msgstr "Beteende" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 msgid "Pause By Locking The Screen" msgstr "Pausa genom att låsa skärmen" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 msgid "Confirm Starting a Break" msgstr "Bekräfta start av rast" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 msgid "Confirm Starting a Pomodoro" msgstr "Bekräfta start av pomodoro" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, c-format msgid "A single session will take %s." msgstr "En ensam session kommer ta %s." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% av tiden kommer vara avsatt för raster." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 msgid "Apply changes to ongoing Pomodoro?" msgstr "Tillämpa ändringar på pågående pomodoro?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 msgid "Apply changes to ongoing break?" msgstr "Tillämpa ändringar på pågående rast?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 msgid "Apply" msgstr "Tillämpa" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "Sidopanel" #~ msgid "Time management utility" #~ msgstr "Tidshanteringsverktyg" #~ msgid "pomodoro;timer;" #~ msgstr "pomodoro;timer;tidtagare;" #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "Behåll fokus genom att regelbundet ta raster" #~ msgid "Visual and audio notifications" #~ msgstr "Visuella aviseringar och ljudaviseringar" #~ msgid "Time tracking and statistics" #~ msgstr "Tidsspårning och statistik" #~ msgid "GNOME desktop integration" #~ msgstr "GNOME-skrivbordsintegration" #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "Kör anpassade kommandon efter pomodoro eller rast" #~ msgid "15 seconds" #~ msgstr "15 sekunder" #~ msgid "30 seconds" #~ msgstr "30 sekunder" #~ msgid "1 minute" #~ msgstr "1 minut" #~ msgid "2 minutes" #~ msgstr "2 minuter" #~ msgid "3 minutes" #~ msgstr "3 minuter" #~ msgid "5 minutes" #~ msgstr "5 minuter" #~ msgid "Compact timer" #~ msgstr "Kompakt tidtagare" #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.28.1" #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "La till tamilsk översättning (tack @omeritzics)" #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "La till hebreisk översättning (tack @Killersparrow1)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.28.0" #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "Stöd för GNOME Shell 49 (tack @aleasto)" #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "Uppdaterade tysk översättning (tack @daPhipz)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.27.0" #~ msgid "Support for GNOME Shell 48" #~ msgstr "Stöd för GNOME Shell 48" #~ msgid "Split time spent across midnight" #~ msgstr "Dela upp tid som spenderats över midnatt" #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "La till telugisk översättning (tack @SpaciousCoder78)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.26.0" #~ msgid "Support for GNOME Shell 47" #~ msgstr "Stöd för GNOME Shell 47" #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "Tillåt avvisande av skärmöverlägg genom en gest då en video spelas" #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "La till georgisk översättning (tack @NorwayFun)" #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "Justerade översättningar i appdata (tack @yakushabb)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.25.2" #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "Fixa bevarande av avisering efter att pomodoro förlängts" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.25.1" #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "Fixar för Gnome Shell 46" #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "Ta bort stöd för Gnome Shell 45" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "Översikt över ändringar i gnome-pomodoro 0.25.0" #~ msgid "Support for GNOME Shell 46" #~ msgstr "Stöd för GNOME Shell 46" #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "Justera byggskript till meson 0.59.0 (tack @mattst88)" #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "" #~ "Låt Pomodoro hantera systemaviseringar under tiden tidtagaren körs" #~ msgid "Timer Ticking" #~ msgstr "Tickande äggklocka" #~ msgid "Birds" #~ msgstr "Fåglar" focustimerhq-FocusTimer-8581be2/po/ta.po000066400000000000000000002243751520625676500202560ustar00rootroot00000000000000# Tamil translation for focus-timer # Copyright (c) 2025 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Killersparrow1 , 2025. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2025-10-30 22:00+0530\n" "Last-Translator: Killersparrow1 \n" "Language-Team: Tamil\n" "Language: ta\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n!=1);\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "உங்கள் நேரத்தை சிறிய அமர்வுகளாகவும் இடைவெளிகளாகவும் பிரிப்பதன் மூலம் திறம்பட வேலை செய்ய " "உதவும் ஒரு செயலி. 25 நிமிடங்கள் வேலை செய்து, பின் 5 நிமிடங்கள் இடைவெளி எடுப்பதன் மூலம் " "உங்கள் கவனத்தை ஒருமுகப்படுத்தலாம்." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "முக்கிய அம்சங்கள்:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "வேலை நேரம் மற்றும் இடைவெளி நேரத்தைத் தனிப்பயனாக்கும் வசதி" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "இடைவேளையின் போது திரை மேலடுக்கு" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "நேரங்காட்டி" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "தினசரி புள்ளிவிவரங்கள்" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "மாதாந்திர புள்ளிவிவரங்கள்" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "விருப்பத்தேர்வுகள்" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "திரை மேலடுக்கு" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "தொடங்கு அல்லது நிறுத்து" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "தொடங்கு, இடைநிறுத்து அல்லது மீண்டும் தொடங்கு" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "பொமோடோரோ தொடங்கு" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "தொடங்கு" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "நிறுத்து" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "இடைநிறுத்து" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "மீண்டும் தொடங்கு" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "தவிர்" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "பின்னோக்கிச் செல்" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 #, fuzzy msgid "Extend current pomodoro or break" msgstr "தற்போதைய பொமோடோரோ அல்லது இடைவேளையை நீட்டி" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "மீட்டமை" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 #, fuzzy msgid "Show preferences" msgstr "விருப்பத்தேர்வுகளைக் காட்டு" #: src/application.vala:203 #, fuzzy msgid "Quit application" msgstr "செயலியை விட்டு வெளியேறு" #: src/application.vala:206 #, fuzzy msgid "Print version information and exit" msgstr "பதிப்புத் தகவலை அச்சிட்டு வெளியேறு" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "முன்னிலைக்குக் கொண்டுவா" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "இன்னும் %s உள்ளது" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "தனிப்பயன் செயல் \"%s\" தோல்வியுற்றது" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "நேர வரம்பை எட்டியது" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "கட்டளையை இயக்க முடியவில்லை" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "கட்டளை காலியாக உள்ளது" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "மூடப்படாத மேற்கோள் குறி" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "தவறான கட்டளை" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "தெரியாத மாறி \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "தெரியாத வடிவம் \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "நிரல் \"%s\" காணப்படவில்லை" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "செயல்கள்" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "தலைகீழ் எண்ணுதல்" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "அமர்வு" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "மற்றவை" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "நேரங்காட்டி தொடங்கப்பட்டது." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "நேரங்காட்டி கைமுறையாக நிறுத்தப்பட்டது." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "நேரங்காட்டி கைமுறையாக இடைநிறுத்தப்பட்டுள்ளது. திரை பூட்டப்படும்போதோ அல்லது கணினி " "உறக்கநிலைக்குச் செல்லும்போதோ இது நிகழாது." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "நேரங்காட்டி கைமுறையாக மீண்டும் தொடங்கப்பட்டது." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "நேரங்காட்டி முடிவதற்கு முன்பே அடுத்த நேரப் பகுதிக்குச் சென்றது." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "பின்னோக்கிச் செல்லும் செயல் பயன்படுத்தப்பட்டது." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "அமர்வு கைமுறையாக அழிக்கப்பட்டது." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "முடிந்தது" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "" "நேரங்காட்டி முடிந்தது. உறுதிப்படுத்தலுக்காக காத்திருந்தால், நேர அளவை மாற்றியமைக்கலாம்." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "மாற்றப்பட்டது" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "நேரங்காட்டி தொடர்பான ஏதேனும் மாற்றம் ஏற்படும்போது தூண்டப்படும்." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "முன்னேற்றத்தை உறுதிப்படுத்து" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "அடுத்த நேரப் பகுதியைத் தொடங்க கைமுறை உறுதிப்படுத்தல் தேவை." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "முன்னேறியது" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "அடுத்த நேரப் பகுதிக்கு மாறியது அல்லது தவிர்ந்தது." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "நிலை மாற்றப்பட்டது" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "அடுத்த நேரப் பகுதிக்கு மாறியது." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "மறுஅட்டவணை செய்யப்பட்டது" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "திட்டமிடப்பட்ட நேரப் பகுதிகள் மாறும்போது தூண்டப்படும்." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "காலாவதியானது" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "செயல்பாடின்மை காரணமாக அமர்வு மீளமைக்கப்படும்போது தூண்டப்படும்." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "பொமோடோரோ" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "ஓர் இடைவெளி எடு" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "ஒரு சிறு இடைவெளி எடு" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "ஒரு நீண்ட இடைவெளி எடு" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "பொமோடோரோ விரைவில் முடிவடைகிறது" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "ஓர் இடைவெளி எடு" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "இடைவெளி விரைவில் முடிவடைகிறது" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 நிமிடம்" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "தயாராகுங்கள்..." #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "பொமோடோரோ முடிந்தது!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "இடைவெளி முடிந்தது!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "பொமோடோரோ தொடங்குவதை உறுதிப்படுத்துங்கள்…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "இடைவெளி தொடங்குவதை உறுதிப்படுத்துங்கள்…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "சிறு இடைவெளி தொடங்குவதை உறுதிப்படுத்துங்கள்…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "நீண்ட இடைவெளி தொடங்குவதை உறுதிப்படுத்துங்கள்…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "இடைவெளியைத் தவிர்" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "ஒலியைத் தொடங்குவதில் தோல்வி" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "கோப்பு காணப்படவில்லை" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "கோப்பு வகை ஆதரிக்கப்படவில்லை" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "நிறுத்தப்பட்டது" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 #, fuzzy msgid "Break" msgstr "இடைவெளி" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "சிறு இடைவெளி" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "நீண்ட இடைவெளி" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, c-format msgid "%uh" msgstr "%u ம" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, c-format msgid "%um" msgstr "%u நி" #: src/core/utils.vala:72 #, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u மணிநேரம்" msgstr[1] "%u மணிநேரங்கள்" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u நிமிடம்" msgstr[1] "%u நிமிடங்கள்" #: src/core/utils.vala:90 #, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u வினாடி" msgstr[1] "%u வினாடிகள்" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "தற்போதைய நிகழ்வின் துல்லியமான நேரம்." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "பொமோடோரோ சுழற்சியின் தற்போதைய நிலை." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "தற்போதைய நேரப் பகுதியின் நிலை." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "நேரங்காட்டி தொடங்கியுள்ளதைக் குறிக்கும் குறியீடு." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "நேரங்காட்டி இடைநிறுத்தப்பட்டுள்ளதைக் குறிக்கும் குறியீடு." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "நேரங்காட்டி முடிந்துவிட்டதைக் குறிக்கும் குறியீடு." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "நேரங்காட்டி ஓடிக்கொண்டிருப்பதைக் குறிக்கும் குறியீடு." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "தற்போதைய நேரங்காட்டியின் கால அளவு." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "கடந்த நேரத்திற்கும் உண்மையான நேரத்திற்கும் இடையிலான வேறுபாடு." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "நேரங்காட்டியில் செலவிடப்பட்ட நேரம்." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "நேரங்காட்டி முடிவதற்கு முன் மீதமுள்ள நேரம்." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "நேரங்காட்டி தொடங்கிய நேரம்." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell நீட்டிப்பு" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "சிறந்த அனுபவத்தைப் பெறுங்கள்!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "டெஸ்க்டாப் ஒருங்கிணைப்பிற்கு GNOME Shell நீட்டிப்பை செயல்படுத்தவும்" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "எப்போதும் அணுகும் தூரத்தில்" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "செயலியைத் திறக்காமல் மேல் பட்டியில் இருந்தே நேரங்காட்டியை நிர்வகிக்கவும்" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "குறைவான கவனச்சிதறல்கள்" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "நேர்த்தியான இடைவெளி நினைவூட்டல்கள்" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "இடைவெளி எடுப்பதை இனிமையான அனுபவமாக்கும் முழுத்திரை மேலடுக்கு" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "முயற்சி செய்யத் தயாரா?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "நீட்டிப்பை நிறுவு (_I)" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "இப்போது வேண்டாம் (_N)" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "ஏதோ தவறு நடந்துவிட்டது" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "நகலெடு" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "மீண்டும் முயற்சி செய் (_T)" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "கைவிடு (_A)" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "கால அளவு முடிந்தது" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "நீட்டிப்புகளை நிறுவ அனுமதி இல்லை" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "நீட்டிப்பைத் தரவிறக்க முடியவில்லை" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "திரை மேலடுக்கு" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "டெஸ்க்டாப்" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell நீட்டிப்பு கிடைக்கிறது" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "மேலும் அறிய" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "பயன்படுத்தப்படாதது" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "முடிந்தது!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "புள்ளிவிவரங்கள்" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "வெளியேறு" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "பதிவு" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "வெற்றுப் பதிவு" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "நீங்கள் நேரங்காட்டியைத் தொடங்கியதும் பதிவுகள் இங்கே தோன்றும்." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "சூழல்" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "கட்டளை" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "வெளியீடு" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "பிழை" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "வெளியேறும் குறியீடு:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "இயக்க நேரம்:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Killersparrow1 https://github.com/Killersparrow1 2025" #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "நன்கொடை அளிக்கவும்" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "இடைவெளிகள்" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "குறுக்கீடுகள்" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "இடைவெளி விகிதம்" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "நாள்" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "வாரம்" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "மாதம்" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "இங்கே பார்ப்பதற்கு இன்னும் எதுவும் இல்லை" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "இதைப் பூர்த்தி செய்ய சில பொமோடோரோக்களை முடிக்கவும்!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u நாள் தவிர்ந்தது" msgstr[1] "%u நாட்கள் தவிர்ந்தன" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u வாரம் தவிர்ந்தது" msgstr[1] "%u வாரங்கள் தவிர்ந்தன" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u மாதம் தவிர்ந்தது" msgstr[1] "%u மாதங்கள் தவிர்ந்தன" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "இன்று" #: src/ui/main/stats/stats-view.vala:1046 #, fuzzy msgid "Yesterday" msgstr "நேற்று" #: src/ui/main/stats/stats-view.vala:1067 #, fuzzy msgid "This week" msgstr "இந்த வாரம்" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "வாரம் %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "%u-ல் %u வாரம்" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "_பொமோடோரோ" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "_சிறு இடைவெளி" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "_நீண்ட இடைவெளி" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "இடைவெளி (_B)" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "திரை மேலடுக்கைத் திற" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "அமர்வு காலாவதியானது" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "இன்னும் %s-ல் நீண்ட இடைவெளி வரும்" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "ஒரு நிமிடம் பின்னோக்கிச் செல்" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "சிறிய காட்சி (_C)" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "_விருப்பத்தேர்வுகள்" #: src/ui/main/window.ui:19 msgid "_About" msgstr "_பற்றி" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "_வெளியேறு" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "முன்மை மெனு" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "நேரங்காட்டி ஓடிக்கொண்டிருக்க வேண்டுமா?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "" "இதை நீங்கள் பின்னணியில் ஓட விடலாம் — அறிவிப்புகள் மற்றும் விசைப்பலகை குறுக்குவழிகள் " "தொடர்ந்து வேலை செய்யும்." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "பின்னணியில் இயக்கு" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "இடைவெளி எடுக்க வேண்டிய நேரம்" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "முதன்மைச் சாளரம்" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "இருண்ட தீம் பயன்படுத்தவும்" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "சிறிய காட்சியினைப் பயன்படுத்தவும்" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "தொடங்கியது" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "இடைநிறுத்தப்பட்டது" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "தனிப்பயன் செயலைத் திருத்து" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "_ரத்துசெய்" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "சேமி (_S)" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "பெயர்" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "தூண்டுதல்" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "நிகழ்வு" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "ஒரு நிகழ்விற்குப் பிறகு கட்டளையை இயக்கு." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "நிபந்தனை" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "நிபந்தனை பூர்த்தியாகாதபோது இரண்டாவது கட்டளையை இயக்குவதை உறுதிசெய்க." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "நிகழ்வுகள்" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "நிகழ்வைச் சேர் (_E)" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "வடிகட்டி (_F)" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "வடிகட்டி" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "ஷெல் கட்டளை" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "கட்டளைகள்" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "நிபந்தனை பூர்த்தியானதும் இயங்கும் கட்டளை" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "நிபந்தனை பூர்த்தியாகாதபோது இயங்கும் கட்டளை" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "வேலை செய்யும் அடைவு" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "Subshell பயன்படுத்தவும்" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "sh -c '' போன்ற subshell மூலம் நிரலை இயக்கு" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "உள்ளீட்டுத் தரவை வழங்கவும்" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "மாறிகளுக்குப் பதில் நீங்கள் JSON பொருளைப் பயன்படுத்தலாம்." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "முடிவடையும் வரை காத்திருக்கவும்" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "இந்தக் கட்டளை முடியும் வரை பிற கட்டளைகளைத் தடைசெய்யவும்." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "செயலை நீக்கு (_D)" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "இன்னும் நிகழ்வுகள் எதுவும் குறிப்பிடப்படவில்லை." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "தனிப்பயன் செயலைச் சேர்" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "சேர் (_A)" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "வேலை செய்யும் அடைவைத் தேர்ந்தெடு" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "_தேர்ந்தெடு" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "தலைப்பில்லாத செயல்" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "நிபந்தனையைச் சேர்" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "குழுவைச் சேர்" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "AND" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "OR" #: src/ui/preferences/automation/action/condition-widget.ui:26 msgid "Is" msgstr "Is" #: src/ui/preferences/automation/action/condition-widget.ui:27 msgid "Is Not" msgstr "Is Not" #: src/ui/preferences/automation/action/condition-widget.ui:39 msgid "Equals" msgstr "Equals" #: src/ui/preferences/automation/action/condition-widget.ui:40 msgid "Greater Than" msgstr "Greater Than" #: src/ui/preferences/automation/action/condition-widget.ui:41 msgid "Less Than" msgstr "Less Than" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "ஆம்" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "இல்லை" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "நிமிடங்கள்" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "வினாடிகள்" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "மணிநேரம்" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "புலத்தைத் தேர்ந்தெடு…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "நிலை" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "ஓடிக்கொண்டிருக்கிறது" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "கால அளவு" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "மாறியைச் செருகு" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "வடிவம்" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "பதிவு (_L)" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "இயக்கப் பதிவைக் காட்டு" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "நேரங்காட்டி நிகழ்வுகள் அல்லது நிபந்தனைகளின் போது ஷெல் கட்டளைகளை தானாக இயக்கவும். மேலும் அறிய." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "குறுக்குவழியை அமை" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "அமை (_S)" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "முடக்கப்பட்டது" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "" "ரத்து செய்ய Esc-ஐ அழுத்தவும் அல்லது குறுக்குவழியை முடக்க Backspace-ஐ " "அழுத்தவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "செயலி திரையில் இல்லாதபோதும் அதை நிர்வகிக்க உலகளாவிய குறுக்குவழிகள் உதவுகின்றன." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "உலகளாவிய குறுக்குவழிகளைத் திருத்த அமைப்புகளைத் திறக்கவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "திருத்து (_E)" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "நேரங்காட்டியைத் தொடங்க அல்லது நிறுத்த புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "நேரங்காட்டியைத் தொடங்க/இடைநிறுத்த/தொடர புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "நேரங்காட்டியைத் தொடங்க புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "நேரங்காட்டியை நிறுத்த புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "நேரங்காட்டியை இடைநிறுத்த புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "நேரங்காட்டியை மீண்டும் தொடர புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "தவிர்ப்பதற்கு புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "ஒரு நிமிடம் பின்னோக்கிச் செல்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "பின்னோக்கிச் செல்ல புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "சாளரத்தை முன்னிலைக்குக் கொண்டுவர புதிய குறுக்குவழியை உள்ளிடவும்" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "அறிவிப்புகள்" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "நேரம் முடிந்து கொண்டிருக்கிறது" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "பொமோடோரோ அல்லது இடைவெளி முடியும்போது அறிவிக்கவும்." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "கட்டாயம் இடைவெளி எடுக்கச் செய்யும் முழுத்திரை அறிவிப்பு." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "பூட்டு தாமதம்" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "திரையைப் பூட்ட வேண்டிய செயல்பாடற்ற காலம்." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "மறுதிறப்பு தாமதம்" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "மேலடுக்கு நீக்கப்பட்ட பிறகு அதை மீண்டும் திறப்பதற்கான கால அளவு." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "ஒருபோதும் வேண்டாம்" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "அறிவிப்புகள்" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "ஒலிகள்" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "தோற்றம்" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "விசைப்பலகை குறுக்குவழி" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "தானியக்கம்" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "ஒலிகள் முடக்கப்பட்டுள்ளன" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "எச்சரிக்கை ஒலிகள்" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "பொமோடோரோ முடிந்ததற்கான ஒலி" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "இடைவெளி முடிந்ததற்கான ஒலி" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "இடிக்கும் ஒலி" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "மணி" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "சத்தமான மணி" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "கடிகார இடித்தல்" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "எதுவுமில்லை" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "ஒலியளவு:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "தனிப்பயன் ஒலியைத் தேர்ந்தெடு" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "பொமோடோரோ நேரம்" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "சிறு இடைவெளி நேரம்" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "நீண்ட இடைவெளி நேரம்" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "சுழற்சிகளின் எண்ணிக்கை" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "நடத்தை" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "திரையைப் பூட்டும்போது இடைநிறுத்து" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "இடைவெளி தொடங்குவதை உறுதிப்படுத்து" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "பொமோடோரோ தொடங்கு" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "ஒரு அமர்வு %s எடுக்கும்." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "நேரத்தில் %u%% இடைவெளிகளுக்காக ஒதுக்கப்படும்." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "நடந்து கொண்டிருக்கும் பொமோடோரோவில் மாற்றங்களைப் பயன்படுத்தவா?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "நடந்து கொண்டிருக்கும் இடைவெளியில் மாற்றங்களைப் பயன்படுத்தவா?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "பயன்படுத்து" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 #, fuzzy msgctxt "accessibility" msgid "Sidebar" msgstr "பக்கவாட்டுப் பட்டை" #, fuzzy #~ msgid "Time management utility" #~ msgstr "நேர மேலாண்மை பயன்பாடு" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "அடிக்கடி இடைவெளிகள் எடுப்பதன் மூலம் கவனத்தைத் தக்கவைத்துக்கொள்ளுங்கள்" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "காட்சி மற்றும் ஒலி அறிவிப்புகள்" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "நேர கண்காணிப்பு மற்றும் புள்ளிவிவரங்கள்" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "GNOME டெஸ்க்டாப் ஒருங்கிணைப்பு" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "பொமோடோரோ அல்லது இடைவேளைக்குப் பிறகு தனிப்பயன் கட்டளைகளை இயக்குதல்" #, fuzzy #~ msgid "Compact timer" #~ msgstr "சிறிய நேரங்காட்டி" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "gnome-pomodoro 0.28.1 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "தமிழ் மொழிபெயர்ப்பு சேர்க்கப்பட்டது (@omeritzics-க்கு நன்றி)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "ஹீப்ரு மொழிபெயர்ப்பு சேர்க்கப்பட்டது (@Killersparrow1-க்கு நன்றி)" #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "gnome-pomodoro 0.28.0 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "GNOME Shell 49-க்கான ஆதரவு (@aleasto-க்கு நன்றி)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "ஜெர்மன் மொழிபெயர்ப்பு புதுப்பிக்கப்பட்டது (@daPhipz-க்கு நன்றி)" #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "gnome-pomodoro 0.27.0 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "GNOME Shell 48-க்கான ஆதரவு" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "நள்ளிரவைத் தாண்டிய நேரத்தைப் பிரித்தல்" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "தெலுங்கு மொழிபெயர்ப்பு சேர்க்கப்பட்டது (@SpaciousCoder78-க்கு நன்றி)" #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "gnome-pomodoro 0.26.0 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "GNOME Shell 47-க்கான ஆதரவு" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "வீடியோ ஓடும்போது சைகை மூலம் திரை மேலடுக்கை நீக்க அனுமதி" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "ஜார்ஜிய மொழிபெயர்ப்பு சேர்க்கப்பட்டது (@NorwayFun-க்கு நன்றி)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "appdata-ல் மொழிபெயர்ப்புகள் சரிசெய்யப்பட்டன (@yakushabb-க்கு நன்றி)" #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "gnome-pomodoro 0.25.2 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "பொமோடோரோவை நீட்டித்த பிறகும் அறிவிப்பு நீடிப்பது சரி செய்யப்பட்டது" #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "gnome-pomodoro 0.25.1 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "GNOME Shell 46-க்கான திருத்தங்கள்" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "GNOME Shell 45-க்கான ஆதரவு நீக்கப்பட்டது" #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "gnome-pomodoro 0.25.0 மாற்றங்களின் கண்ணோட்டம்" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "GNOME Shell 46-க்கான ஆதரவு" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "பில்ட் ஸ்கிரிப்ட் meson 0.59.0-க்கு மாற்றப்பட்டது (@mattst88-க்கு நன்றி)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "நேரங்காட்டி ஓடும்போது Pomodoro அறிவிப்புகளை நிர்வகிக்கட்டும்" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 வினாடிகள்" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 வினாடிகள்" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 நிமிடம்" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 நிமிடங்கள்" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 நிமிடங்கள்" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 நிமிடங்கள்" #~ msgid "Timer Ticking" #~ msgstr "நேரங்காட்டி இடித்தல்" #, fuzzy #~ msgid "Birds" #~ msgstr "பறவைகள்" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "நேரங்காட்டி;" #, fuzzy #~ msgid "Start/Stop" #~ msgstr "தொடங்கு/நிறுத்து" #, fuzzy #~ msgid "Pause/Resume" #~ msgstr "இடைநிறுத்து/தொடர்" #, fuzzy #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "பொமோடோரோ அல்லது இடைவேளைக்குத் தவிர்" #, fuzzy #~ msgid "Reset current session" #~ msgstr "தற்போதைய அமர்வை மீட்டமை" #, fuzzy #~ msgid "About Pomodoro" #~ msgstr "பொமோடோரோ" #~ msgid "A simple time management utility" #~ msgstr "ஒரு எளிய நேர நிர்வாக பயன்பாடு" #, fuzzy #~ msgid "_Stopped" #~ msgstr "நிறுத்து" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "GNOME ஷெல்லுக்கான காட்டொளி" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "நீண்ட இடைவெளி நேரம்" #~ msgid "A time management utility for GNOME" #~ msgstr "GNOME-க்கான ஒரு நேர நிர்வாக செயலி" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "பொமோடோரோ நுட்பத்தின்படி நேரத்தை நிர்வகிக்க உதவும் ஒரு GNOME செயலி. இது ஒவ்வொரு 25 " #~ "நிமிட வேலைக்குப் பிறகும் ஒரு சிறிய இடைவெளி எடுப்பதன் மூலம் உற்பத்தித்திறன் மற்றும் " #~ "கவனத்தை மேம்படுத்த உதவுகிறது." #~ msgid "Timer window" #~ msgstr "நேரங்காட்டி சாளரம்" #~ msgid "Indicator for GNOME Shell" #~ msgstr "GNOME ஷெல்லுக்கான காட்டொளி" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "GNOME ஷெல்லுக்கான காட்டொளி" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "GNOME ஷெல்லுக்கான காட்டொளி" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "GNOME ஷெல்லுக்கான காட்டொளி" #~ msgid "_Timer" #~ msgstr "_நேரங்காட்டி" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "" #~ "நேரங்காட்டியைக் கட்டுப்படுத்த விசைப்பலகை குறுக்குவழி. மாற்ற புதிய குறுக்குவழியை " #~ "உள்ளிடவும்." #~ msgid "Pomodoros before a long break" #~ msgstr "நீண்ட இடைவெளிக்கு முன் பொமோடோரோக்களின் எண்ணிக்கை" #~ msgid "Screen notifications" #~ msgstr "திரை அறிவிப்புகள்" #~ msgid "Wait for activity after a break" #~ msgstr "இடைவெளிக்குப் பிறகு செயல்பாட்டிற்காக காத்திரு" #~ msgid "Plugins…" #~ msgstr "பிளக்-இன்கள்..." #~ msgid "Plugins" #~ msgstr "பிளக்-இன்கள்" #~ msgid "Back" #~ msgstr "பின்செல்" #~ msgid "Complete a few sessions" #~ msgstr "சில அமர்வுகளை முடிக்கவும்" #~ msgid "Previous (Alt+Left)" #~ msgstr "முந்தையது (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "அடுத்தது (Alt+Right)" #~ msgid "Complete" #~ msgstr "முடிப்பது" #~ msgid "Enable" #~ msgstr "இயக்கு" #~ msgid "Add" #~ msgstr "சேர்" #~ msgid "Remove" #~ msgstr "நீக்கு" #~ msgid "Elapsed Time" #~ msgstr "கடந்துபோன நேரம்" #~ msgid "Pause Timer" #~ msgstr "நேரங்காட்டியை இடைநிறுத்து" #~ msgid "Pause break" #~ msgstr "இடைவெளியை இடைநிறுத்து" #~ msgid "Pause Pomodoro" #~ msgstr "பொமோடோரோவை இடைநிறுத்து" #~ msgid "Resume break" #~ msgstr "இடைவெளியை மீண்டும் தொடங்கு" #~ msgid "Resume Pomodoro" #~ msgstr "பொமோடோரோவை மீண்டும் தொடங்கு" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "இன்னும் %d நிமிடம் உள்ளது" #~ msgstr[1] "இன்னும் %d நிமிடங்கள் உள்ளன" #~ msgid "Report issue" #~ msgstr "பிரச்சினையை புகாரளி" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "%s சேவையை இயக்க முடியவில்லை" #~ msgid "Woodland Birds" #~ msgstr "காட்டுப் பறவைகள்" #~ msgid "End of Break Sound" #~ msgstr "இடைவெளி முடியும் ஒலி" #~ msgid "Off" #~ msgstr "அணை" #~ msgid "Ticking sound" #~ msgstr "இடிக்கும் ஒலி" #~ msgid "Start of break sound" #~ msgstr "இடைவெளி தொடங்கும் ஒலி" #~ msgid "End of break sound" #~ msgstr "இடைவெளி முடியும் ஒலி" #~ msgid "A time management utility for GNOME." #~ msgstr "GNOME-க்கான நேர மேலாண்மை பயன்பாடு." focustimerhq-FocusTimer-8581be2/po/te.po000066400000000000000000002276551520625676500202660ustar00rootroot00000000000000# Telugu translation for focus-timer # Copyright (c) 2024 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Aryan Karamtoth , 2024. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2024-10-08 09:33+0530\n" "Last-Translator: Aryan Karamtoth \n" "Language-Team: Telugu\n" "Language: te\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Gtranslator 47.0\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "మీ సమయాన్ని ఏకాగ్రతతో కూడిన పని సెషన్‌లుగా మరియు చిన్న విరామాలుగా విభజించడం ద్వారా మీరు మరింత ప్రభావవంతంగా " "పనిచేయడానికి సహాయపడే ఉత్పాదకత టైమర్. ఏకాగ్రతను కాపాడుకోవడానికి మరియు అలసటను నివారించడానికి 25 నిమిషాలు పని " "చేయండి, ఆపై 5 నిమిషాల విరామం తీసుకోండి." #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "ముఖ్య లక్షణాలు:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "అనుకూలీకరించదగిన పని సెషన్ మరియు విరామ సమయాలు" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "విరామ సమయంలో స్క్రీన్ ఓవర్లే" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "కామిల్ ప్రుస్కో" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "టైమర్" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "రోజువారీ గణాంకాలు" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "నెలవారీ గణాంకాలు" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "ప్రాధాన్యతలు" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "స్క్రీన్ ఓవర్లే" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "ప్రారంభించు లేదా ఆపు" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "ప్రారంభించు, పాజ్ లేదా కొనసాగించు" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "పోమోడోరో ప్రారంభించండి" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "ప్రారంభించు" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "ఆపు" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "పాజ్" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "కొనసాగించు" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "దాటవేయి" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "వెనక్కి తిప్పు" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "ప్రస్తుత పోమోడోరోను లేదా విరామాన్ని పొడిగించండి" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 msgid "Reset" msgstr "రీసెట్" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "ప్రాధాన్యతలను చూపించు" #: src/application.vala:203 msgid "Quit application" msgstr "అప్లికేషన్ నుండి నిష్క్రమించండి" #: src/application.vala:206 msgid "Print version information and exit" msgstr "సంస్కరణ సమాచారాన్ని ముద్రించి నిష్క్రమించండి" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "ముందుకి తీసుకురా" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "%s మిగిలి ఉంది" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "అనుకూల చర్య \"%s\" విఫలమైంది" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "సమయం ముగిసింది" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "ఆదేశాన్ని అమలు చేయడంలో విఫలమైంది" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "ఆదేశం ఖాళీగా ఉంది" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "ముగించని కొటేషన్ మార్క్" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "చెల్లని ఆదేశం" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "తెలియని వేరియబుల్ \"%s\"" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "తెలియని ఫార్మాట్ \"%s\"" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "ప్రోగ్రామ్ \"%s\" కనుగొనబడలేదు" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "చర్యలు" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "కౌంట్‌డౌన్" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 msgid "Session" msgstr "సెషన్" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "ఇతర" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "టైమర్ ప్రారంభించబడింది." #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "టైమర్ మాన్యువల్‌గా ఆపబడింది." #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "" "కౌంట్‌డౌన్ మాన్యువల్‌గా పాజ్ చేయబడింది. స్క్రీన్ లాక్ చేసినప్పుడు లేదా సిస్టమ్ సస్పెండ్ చేసినప్పుడు ఇది ట్రిగ్గర్ " "అవ్వదు." #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "కౌంట్‌డౌన్ మాన్యువల్‌గా కొనసాగించబడింది." #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "కౌంట్‌డౌన్ ముగియకముందే తదుపరి సమయ విభాగానికి వెళ్లారు." #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "వెనక్కి తిప్పే చర్య ఉపయోగించబడింది. ఇది గతంలో ఒక పాజ్‌ని జోడిస్తుంది." #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "సెషన్ మాన్యువల్‌గా తొలగించబడింది." #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 msgid "Finished" msgstr "పూర్తయింది" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "కౌంట్‌డౌన్ ముగిసింది. నిర్ధారణ కోసం వేచి ఉంటే, సమయ విభాగం యొక్క వ్యవధిని ఇంకా మార్చవచ్చు." #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "మారింది" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "కౌంట్‌డౌన్‌కు సంబంధించిన ఏవైనా మార్పులపైన ట్రిగ్గర్ చేయబడుతుంది." #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "పురోగతిని నిర్ధారించండి" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "తదుపరి సమయ విభాగాన్ని ప్రారంభించడానికి మాన్యువల్ నిర్ధారణ అవసరం." #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "ముందుకు వెళ్ళారు" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "తదుపరి సమయ విభాగానికి మారారు లేదా దాటవేశారు." #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "స్థితి మారింది" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "తదుపరి సమయ విభాగానికి మారినప్పుడు లేదా విరామం మళ్ళీ లేబుల్ చేయబడినప్పుడు." #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "మళ్ళీ షెడ్యూల్ చేయబడింది" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "షెడ్యూల్ చేయబడిన సమయ విభాగాలు మారినప్పుడు ట్రిగ్గర్ చేయబడుతుంది." #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "గడువు ముగిసింది" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "కార్యాచరణ లేకపోవడం వల్ల సెషన్ రీసెట్ కాబోతున్నప్పుడు ట్రిగ్గర్ చేయబడుతుంది." #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "పోమోడోరో" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "విరామం తీసుకోండి" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "చిన్న విరామం తీసుకోండి" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "పెద్ద విరామం తీసుకోండి" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "పోమోడోరో ముగియబోతోంది" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "విరామం తీసుకోండి" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "విరామం ముగియబోతోంది" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 నిమిషం" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "సిద్ధంగా ఉండండి..." #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "పోమోడోరో ముగిసింది!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "విరామం ముగిసింది!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "పోమోడోరో ప్రారంభాన్ని ధృవీకరించండి..." #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "విరామం ప్రారంభాన్ని ధృవీకరించండి..." #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "చిన్న విరామం ప్రారంభాన్ని ధృవీకరించండి..." #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "పెద్ద విరామం ప్రారంభాన్ని ధృవీకరించండి..." #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "విరామాన్ని దాటవేయి" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "ప్లేబ్యాక్ ప్రారంభించడంలో విఫలమైంది" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "ఫైల్ కనుగొనబడలేదు" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "ఫైల్ రకానికి మద్దతు లేదు" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "ఆపబడింది" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "విరామం" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "చిన్న విరామం" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "పెద్ద విరామం" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u గం" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u ని" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u గంట" msgstr[1] "%u గంటలు" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u నిమిషం" msgstr[1] "%u నిమిషాలు" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u సెకను" msgstr[1] "%u సెకన్లు" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "ప్రస్తుత ఈవెంట్ యొక్క ఖచ్చితమైన సమయం." #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "పోమోడోరో చక్రం యొక్క ప్రస్తుత దశ. సాధ్యమయ్యే విలువలు: stopped, pomodoro, " "break, short-break, long-break." #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "ప్రస్తుత సమయ విభాగం స్థితి. సాధ్యమయ్యే విలువలు: scheduled, in-progress, completed, uncompleted." #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "కౌంట్‌డౌన్ ప్రారంభమైందో లేదో తెలిపే ఫ్లాగ్." #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "కౌంట్‌డౌన్ పాజ్ చేయబడిందో లేదో తెలిపే ఫ్లాగ్." #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "కౌంట్‌డౌన్ ముగిసిందో లేదో తెలిపే ఫ్లాగ్." #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "టైమర్ చురుకుగా కౌంట్‌డౌన్ చేస్తుందో లేదో తెలిపే ఫ్లాగ్." #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "ప్రస్తుత కౌంట్‌డౌన్ వ్యవధి." #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "గడిచిన సమయానికి మరియు గడిచిన అసలు సమయానికి మధ్య వ్యత్యాసం." #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "కౌంట్‌డౌన్‌లో గడిపిన సమయం." #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "కౌంట్‌డౌన్ ముగిసేలోపు మిగిలి ఉన్న సమయం." #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "కౌంట్‌డౌన్ ప్రారంభమైన సమయం." #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "గ్నోమ్ షెల్ ఎక్స్‌టెన్షన్" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "ఉత్తమ అనుభవాన్ని పొందండి!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "అతుకులు లేని డెస్క్‌టాప్ అనుసంధానం కోసం గ్నోమ్ షెల్ ఎక్స్‌టెన్షన్ ని ప్రారంభించండి" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "ఎల్లప్పుడూ అందుబాటులో" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "యాప్ తెరవకుండానే నేరుగా టాప్ బార్ నుండి టైమర్‌ను నియంత్రించండి" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "తక్కువ పరధ్యానం" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "మెరుగైన విరామ రిమైండర్‌లు" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "విరామం తీసుకోవడాన్ని ఆహ్లాదకరంగా మార్చే సొగసైన ఫుల్-స్క్రీన్ ఓవర్లే" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "దీన్ని ప్రయత్నించడానికి సిద్ధంగా ఉన్నారా?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "ఎక్స్‌టెన్షన్‌ను ఇన్‌స్టాల్ చేయండి (_I)" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "ఇప్పుడు వద్దు (_N)" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "ఏదో తప్పు జరిగింది" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "క్లిప్‌బోర్డ్‌కు కాపీ చేయండి" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "మళ్ళీ ప్రయత్నించండి (_T)" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "విరమించు (_A)" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "సమయం ముగిసింది" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "ఎక్స్‌టెన్షన్‌లను ఇన్‌స్టాల్ చేయడం అనుమతించబడదు" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "ఎక్స్‌టెన్షన్‌ను డౌన్‌లోడ్ చేయడంలో విఫలమైంది" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "స్క్రీన్ ఓవర్లే" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "డెస్క్‌టాప్" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "గ్నోమ్ షెల్ ఎక్స్‌టెన్షన్ అందుబాటులో ఉంది" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "మరింత తెలుసుకోండి" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "ఉపయోగించనిది" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "పూర్తయింది!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "గణాంకాలు" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "నిష్క్రమించు" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "లాగ్" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "ఖాళీ లాగ్" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "మీరు టైమర్‌ను ప్రారంభించిన తర్వాత ఎంట్రీలు ఇక్కడ కనిపిస్తాయి." #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "సందర్భం" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "ఆదేశం" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "అవుట్‌పుట్" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "లోపం" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "ఎగ్జిట్ కోడ్:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "అమలు సమయం:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "Aryan Karamtoth " #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "విరాళం ఇవ్వండి" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "విరామాలు" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "అంతరాయాలు" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "విరామ నిష్పత్తి" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "రోజు" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "వారం" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "నెల" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "ఇక్కడ ఇంకా ఏమీ లేదు" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "దీన్ని నింపడానికి కొన్ని పోమోడోరోలను పూర్తి చేయండి!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "%u రోజు దాటవేయబడింది" msgstr[1] "%u రోజులు దాటవేయబడ్డాయి" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "%u వారం దాటవేయబడింది" msgstr[1] "%u వారాలు దాటవేయబడ్డాయి" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "%u నెల దాటవేయబడింది" msgstr[1] "%u నెలలు దాటవేయబడ్డాయి" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "ఈరోజు" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "నిన్న" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "ఈ వారం" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "వారం %u" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "%u లో వారం %u" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "పోమోడోరో (_P)" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "చిన్న విరామం (_S)" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "పెద్ద విరామం (_L)" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "విరామం (_B)" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "స్క్రీన్ ఓవర్లేను తెరువు" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "సెషన్ ముగిసింది" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "పెద్ద విరామం ఇంకో %s లో" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "ఒక నిమిషం వెనక్కి తిప్పు" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "చిన్న రూపు (_C)" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "ప్రాధాన్యతలు (_P)" #: src/ui/main/window.ui:19 msgid "_About" msgstr "గురించి (_A)" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "నిష్క్రమించు (_Q)" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "ప్రధాన మెనూ" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "టైమర్‌ను నడవనివ్వాలా?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "మీరు దీన్ని బ్యాక్‌గ్రౌండ్‌లో నడవనివ్వవచ్చు — నోటిఫికేషన్‌లు మరియు షార్ట్‌కట్‌లు ఇప్పటికీ పనిచేస్తాయి." #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "బ్యాక్‌గ్రౌండ్‌లో నడపండి" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "ఇది విరామం తీసుకోవలసిన సమయం" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "ప్రధాన విండో" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "డార్క్ థీమ్" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "చిన్న రూపు కావాలి" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "ప్రారంభించబడింది" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 #, fuzzy msgid "Paused" msgstr "నిలిపివేయబడింది" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "అనుకూల చర్యను సవరించండి" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "రద్దు (_C)" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "సేవ్ చేయి (_S)" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "పేరు" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "ట్రిగ్గర్" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "ఈవెంట్" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "ఈవెంట్ తర్వాత ఆదేశాన్ని అమలు చేయండి." #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "షరతు" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "షరతు నెరవేరకపోతే రెండో ఆదేశాన్ని అమలు చేసేలా చూడు." #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "ఈవెంట్‌లు" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "ఈవెంట్ జోడించు (_E)" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "ఫిల్టర్ (_F)" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "ఫిల్టర్" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "షెల్ ఆదేశం" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "ఆదేశాలు" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "షరతు నెరవేరినప్పటి ఆదేశం" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "షరతు నెరవేరనప్పటి ఆదేశం" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "వర్కింగ్ డైరెక్టరీ" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "సబ్-షెల్ ఉపయోగించండి" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "ప్రోగ్రామ్‌ను sh -c '' వంటి సబ్-షెల్ నుండి నడపండి" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "ఇన్‌పుట్ డేటాను పంపండి" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "వేరియబుల్స్ పంపే బదులు మీరు JSON ఆబ్జెక్ట్‌ను ప్రాసెస్ చేయవచ్చు." #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "పూర్తయ్యే వరకు వేచి ఉండు" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "ఆదేశం పూర్తయ్యే వరకు ఇతర ఆదేశాలను అమలు చేయకుండా ఆపు." #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "చర్యను తొలగించు (_D)" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "ఇంకా ఏ ఈవెంట్‌లు పేర్కొనబడలేదు." #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "అనుకూల చర్యను జోడించండి" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "జోడించు (_A)" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "వర్కింగ్ డైరెక్టరీని ఎంచుకోండి" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "ఎంచుకోండి (_S)" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "పేరులేని చర్య" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "షరతు జోడించు" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "సమూహాన్ని జోడించు" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "మరియు (AND)" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "లేదా (OR)" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "అవును" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "కాదు" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "సమానం" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "కంటే ఎక్కువ" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "కంటే తక్కువ" #: src/ui/preferences/automation/action/condition-widget.ui:67 msgid "Yes" msgstr "అవును" #: src/ui/preferences/automation/action/condition-widget.ui:68 msgid "No" msgstr "కాదు" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "నిమిషాలు" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "సెకన్లు" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "గంటలు" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "ఫీల్డ్‌ని ఎంచుకోండి..." #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "స్థితి" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "నడుస్తోంది" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "వ్యవధి" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "వేరియబుల్‌ను చొప్పించండి" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "ఫార్మాట్" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "లాగ్ (_L)" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "ఎగ్జిక్యూషన్ లాగ్‌ను చూపించు" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "టైమర్ ఈవెంట్‌లు లేదా షరతులపై షెల్ ఆదేశాలను స్వయంచాలకంగా నడపండి. మరింత తెలుసుకోండి." #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "షార్ట్‌కట్ సెట్ చేయండి" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "సెట్ చేయి (_S)" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "నిలిపివేయబడింది" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "రద్దు చేయడానికి Esc నొక్కండి లేదా షార్ట్‌కట్‌ను నిలిపివేయడానికి Backspace నొక్కండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "గ్లోబల్ షార్ట్‌కట్‌లు యాప్ స్క్రీన్‌పై లేనప్పుడు కూడా దాన్ని నియంత్రించడానికి అనుమతిస్తాయి. యాప్ బ్యాక్‌గ్రౌండ్‌లో " "నడుస్తున్నంత వరకు ఇవి పనిచేస్తాయి." #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "గ్లోబల్ షార్ట్‌కట్‌లను సవరించడానికి యాప్ సెట్టింగ్‌లను తెరవండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "సవరించు (_E)" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "టైమర్‌ను ప్రారంభించడానికి లేదా ఆపడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "టైమర్‌ను ప్రారంభించడానికి/పాజ్ చేయడానికి/కొనసాగించడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "టైమర్‌ను ప్రారంభించడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "టైమర్‌ను ఆపడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "టైమర్‌ను పాజ్ చేయడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "టైమర్‌ను కొనసాగించడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "దాటవేయడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "ఒక నిమిషం వెనక్కి తిప్పు" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "వెనక్కి తిప్పడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "విండోను ముందుకు తీసుకురావడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "ప్రకటనలు" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "సమయం అయిపోతోంది" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "పోమోడోరో లేదా విరామం ముగియబోతున్నప్పుడు తెలియజేయండి." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "ఖచ్చితంగా విరామం తీసుకోవాలని సూచించే ఫుల్-స్క్రీన్ నోటిఫికేషన్." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "లాక్ ఆలస్యం" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "స్క్రీన్ లాక్ చేయడానికి కావాల్సిన నిశ్చల సమయం." #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "తిరిగి తెరిచే ఆలస్యం" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "ఓవర్లే తీసివేసిన తర్వాత తిరిగి తెరవడానికి కావాల్సిన నిశ్చల సమయం." #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "ఎప్పుడూ వద్దు" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "నోటిఫికేషన్‌లు" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "శబ్దాలు" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "కనిపించే తీరు" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "కీబోర్డ్ షార్ట్‌కట్‌లు" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "ఆటోమేషన్" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "శబ్దాలు నిలిపివేయబడ్డాయి" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "అలర్ట్ శబ్దాలు" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "పోమోడోరో పూర్తయిన శబ్దం" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "విరామం పూర్తయిన శబ్దం" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "నేపథ్య శబ్దం" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "గంట (Bell)" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "పెద్ద గంట" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "గడియారం టిక్-టిక్" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "ఏదీ లేదు" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "వాల్యూమ్:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "అనుకూల ధ్వనిని ఎంచుకోండి" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "పోమోడోరో వ్యవధి" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "చిన్న విరామ వ్యవధి" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "పెద్ద విరామ వ్యవధి" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "చక్రాల సంఖ్య" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "ప్రవర్తన" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "స్క్రీన్ లాక్ చేయడం ద్వారా పాజ్ చేయండి" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "విరామం ప్రారంభించే ముందు నిర్ధారించండి" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "పోమోడోరో ప్రారంభించే ముందు నిర్ధారించండి" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "ఒక సెషన్ %s సమయం తీసుకుంటుంది." #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "సమయంలో %u%% విరామాల కోసం కేటాయించబడుతుంది." #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "ప్రస్తుతం నడుస్తున్న పోమోడోరోకు మార్పులను వర్తింపజేయాలా?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "ప్రస్తుతం నడుస్తున్న విరామానికి మార్పులను వర్తింపజేయాలా?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "వర్తింపజేయి" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "సైడ్ బార్" #, fuzzy #~ msgid "Time management utility" #~ msgstr "సమయ నిర్వహణ సాధనం" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "తరచుగా విరామాలు తీసుకోవడం ద్వారా ఏకాగ్రతను కాపాడుకోండి" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "దృశ్య మరియు శ్రవణ నోటిఫికేషన్‌లు" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "సమయ ట్రాకింగ్ మరియు గణాంకాలు" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "గ్నోమ్ డెస్క్‌టాప్ అనుసంధానం" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "పోమోడోరో లేదా విరామం తర్వాత అనుకూల ఆదేశాలను అమలు చేయండి" #, fuzzy #~ msgid "Compact timer" #~ msgstr "చిన్న టైమర్" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "gnome-pomodoro 0.28.1 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "తమిళ అనువాదం జోడించబడింది (ధన్యవాదాలు @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "హీబ్రూ అనువాదం జోడించబడింది (ధన్యవాదాలు @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "gnome-pomodoro 0.28.0 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "గ్నోమ్ షెల్ 49 కోసం మద్దతు (ధన్యవాదాలు @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "జర్మన్ అనువాదం నవీకరించబడింది (ధన్యవాదాలు @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "gnome-pomodoro 0.27.0 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "గ్నోమ్ షెల్ 48 కోసం మద్దతు" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "అర్ధరాత్రి దాటిన సమయాన్ని విభజించండి" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "తెలుగు అనువాదం జోడించబడింది (ధన్యవాదాలు @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "gnome-pomodoro 0.26.0 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "గ్నోమ్ షెల్ 47 కోసం మద్దతు" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "వీడియో ప్లే అవుతున్నప్పుడు సంజ్ఞ ద్వారా స్క్రీన్ ఓవర్లేను తీసివేయడానికి అనుమతించు" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "జార్జియన్ అనువాదం జోడించబడింది (ధన్యవాదాలు @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "యాప్ డేటాలో అనువాదాలు సర్దుబాటు చేయబడ్డాయి (ధన్యవాదాలు @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "gnome-pomodoro 0.25.2 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "పోమోడోరోను పొడిగించిన తర్వాత నోటిఫికేషన్‌ను అలాగే ఉంచేలా పరిష్కరించబడింది" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "gnome-pomodoro 0.25.1 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "గ్నోమ్ షెల్ 46 కోసం పరిష్కారాలు" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "గ్నోమ్ షెల్ 45 మద్దతు నిలిపివేత" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "gnome-pomodoro 0.25.0 లో మార్పుల అవలోకనం" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "గ్నోమ్ షెల్ 46 కోసం మద్దతు" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "బిల్డ్ స్క్రిప్ట్‌ను meson 0.59.0 కి సర్దుబాటు చేయండి (ధన్యవాదాలు @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "టైమర్ నడుస్తున్నప్పుడు సిస్టమ్ నోటిఫికేషన్‌లను పోమోడోరో నిర్వహించనివ్వండి" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 సెకన్లు" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 సెకన్లు" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 నిమిషం" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 నిమిషాలు" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 నిమిషాలు" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 నిమిషాలు" #~ msgid "Timer Ticking" #~ msgstr "టైమర్ టిక్-టిక్" #, fuzzy #~ msgid "Birds" #~ msgstr "పక్షులు" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "టైమర్;" #~ msgid "Start/Stop" #~ msgstr "ప్రారంభించు/ఆపు" #~ msgid "Pause/Resume" #~ msgstr "పాజ్/కొనసాగించు" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "పోమోడోరోకు లేదా విరామానికి వెళ్లండి" #~ msgid "Reset current session" #~ msgstr "ప్రస్తుత సెషన్‌ని రీసెట్ చేయండి" #~ msgid "Run as background service" #~ msgstr "నేపథ్య సేవగా అమలు చేయండి" #~ msgid "About Pomodoro" #~ msgstr "పోమోడోరో గురించి" #~ msgid "A simple time management utility" #~ msgstr "ఒక సాధారణ సమయ నిర్వహణ సాధనం" #, fuzzy #~ msgid "_Stopped" #~ msgstr "ఆపబడింది (_S)" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "గ్నోమ్ షెల్ ఎక్స్‌టెన్షన్ అందుబాటులో ఉంది" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "పెద్ద విరామం ఇంకో %s లో" #~ msgid "A time management utility for GNOME" #~ msgstr "గ్నోమ్ కోసం సమయ నిర్వహణ సాధనం" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "పోమోడోరో పద్ధతి ప్రకారం సమయాన్ని నిర్వహించడంలో సహాయపడే గ్నోమ్ సాధనం. ప్రతి 25 నిమిషాల పని తర్వాత చిన్న " #~ "విరామాలు తీసుకోవడం ద్వారా ఉత్పాదకతను మరియు ఏకాగ్రతను మెరుగుపరచడం దీని ఉద్దేశ్యం." #~ msgid "Timer window" #~ msgstr "టైమర్ విండో" #~ msgid "Indicator for GNOME Shell" #~ msgstr "గ్నోమ్ షెల్ కోసం సూచిక" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "గ్నోమ్ షెల్ 4.0 కోసం మద్దతు" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "గ్నోమ్ షెల్ 3.36 కోసం మద్దతు" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "గ్నోమ్ షెల్ 3.34 కి మాత్రమే మద్దతు" #~ msgid "_Timer" #~ msgstr "టైమర్ (_T)" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "టైమర్‌ను మార్చడానికి కీబోర్డ్ షార్ట్‌కట్. మార్చడానికి కొత్త షార్ట్‌కట్‌ను నమోదు చేయండి." #~ msgid "Pomodoros before a long break" #~ msgstr "పెద్ద విరామం ముందు పోమోడోరోలు" #~ msgid "Keyboard shortcut" #~ msgstr "కీబోర్డ్ షార్ట్‌కట్" #~ msgid "Screen notifications" #~ msgstr "స్క్రీన్ నోటిఫికేషన్‌లు" #~ msgid "Wait for activity after a break" #~ msgstr "విరామం తర్వాత కార్యాచరణ కోసం వేచి ఉండండి" #~ msgid "Plugins…" #~ msgstr "ప్లగిన్‌లు..." #~ msgid "Plugins" #~ msgstr "ప్లగిన్‌లు" #~ msgid "Back" #~ msgstr "వెనుకకు" #~ msgid "Complete a few sessions" #~ msgstr "కొన్ని సెషన్లను పూర్తి చేయండి" #~ msgid "Previous (Alt+Left)" #~ msgstr "మునుపటి (Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "తదుపరి (Alt+Right)" #~ msgid "Complete" #~ msgstr "పూర్తి" #~ msgid "Enable" #~ msgstr "ప్రారంభించు" #~ msgid "Add" #~ msgstr "జోడించు" #~ msgid "Remove" #~ msgstr "తొలగించు" #~ msgid "Elapsed Time" #~ msgstr "గడిచిన సమయం" #~ msgid "Pause Timer" #~ msgstr "టైమర్‌ను పాజ్ చేయండి" #~ msgid "Pause break" #~ msgstr "విరామాన్ని పాజ్ చేయండి" #~ msgid "Pause Pomodoro" #~ msgstr "పోమోడోరోను పాజ్ చేయండి" #~ msgid "Resume break" #~ msgstr "విరామాన్ని కొనసాగించు" #~ msgid "Resume Pomodoro" #~ msgstr "పోమోడోరోను కొనసాగించు" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "%d నిమిషం మిగిలి ఉంది" #~ msgstr[1] "%d నిమిషాలు మిగిలి ఉన్నాయి" #~ msgid "Report issue" #~ msgstr "సమస్యను నివేదించండి" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "%s సేవను నడపడంలో విఫలమైంది" #~ msgid "Woodland Birds" #~ msgstr "అడవి పక్షులు" #~ msgid "End of Break Sound" #~ msgstr "విరామం ముగింపు శబ్దం" #~ msgid "Start of Break Sound" #~ msgstr "విరామం ప్రారంభ శబ్దం" #~ msgid "Off" #~ msgstr "ఆఫ్" #~ msgid "Ticking sound" #~ msgstr "టిక్-టిక్ శబ్దం" #~ msgid "Start of break sound" #~ msgstr "విరామం ప్రారంభ శబ్దం" #~ msgid "End of break sound" #~ msgstr "విరామం ముగింపు శబ్దం" #~ msgid "Focus on your task." #~ msgstr "మీ పనిపై దృష్టి పెట్టండి." #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "మీకు %d నిమిషం ఉంది" #~ msgstr[1] "మీకు %d నిమిషాలు ఉన్నాయి" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "మీకు %d సెకను ఉంది" #~ msgstr[1] "మీకు %d సెకన్లు ఉన్నాయి" #~ msgid "Take a longer break" #~ msgstr "కాస్త పెద్ద విరామం తీసుకోండి" #~ msgid "Lengthen it" #~ msgstr "దీనిని పొడిగించండి" #~ msgid "Shorten it" #~ msgstr "దీనిని కుదించండి" #~ msgid "Start pomodoro" #~ msgstr "పోమోడోరో ప్రారంభించండి" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "\"%s\"ని షార్ట్‌కట్‌గా ఉపయోగించడం టైపింగ్‌కు ఆటంకం కలిగిస్తుంది. Control, Alt లేదా Shift వంటి " #~ "మరో కీని జోడించి ప్రయత్నించండి." #~ msgid "Available" #~ msgstr "అందుబాటులో ఉంది" #~ msgid "Busy" #~ msgstr "బిజీ" #~ msgid "Idle" #~ msgstr "ఖాళీగా ఉంది" #~ msgid "Invisible" #~ msgstr "కనిపించదు" #, c-format #~ msgid "%d m" #~ msgstr "%d ని" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f గం" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f గం" focustimerhq-FocusTimer-8581be2/po/zh_CN.po000066400000000000000000001633631520625676500206520ustar00rootroot00000000000000# Chinese (simplified) translation for focus-timer # Copyright (c) 2020 focus-timer contributors # This file is distributed under the same license as the focus-timer package. # # Authors: # Meng Zhuo , 2012. # msgid "" msgstr "" "Project-Id-Version: focus-timer 1.0-alpha\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2026-05-29 10:23+0200\n" "PO-Revision-Date: 2023-03-26 11:47+0200\n" "Last-Translator: wffger \n" "Language-Team: Chinese\n" "Language: zh_CN\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" "X-Generator: Poedit 3.1.1\n" #. translators: Consider "Concentration Timer" as an alternative. #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:2 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:7 src/main.vala:36 msgid "Focus Timer" msgstr "" #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:3 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:8 msgid "Work with regular breaks" msgstr "" #. Translators: Search terms to find this application. Do NOT translate or localize the semicolons! The list MUST also end with a semicolon! #: data/io.github.focustimerhq.FocusTimer.desktop.in.in:12 msgid "pomodoro;timer;productivity;time tracker;time management;" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:10 #, fuzzy msgid "" "A productivity timer that helps you work more effectively by breaking your " "time into focused work sessions followed by short breaks. Work for 25 " "minutes, then take a 5-minute break to maintain concentration and prevent " "burnout." msgstr "" "一个生产力计时器,通过将您的时间分为专注工作阶段和随后的短暂休息,帮助您更有" "效地工作。工作 25 分钟,然后休息 5 分钟,以保持注意力集中并防止疲劳。" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:14 #, fuzzy msgid "Key features:" msgstr "主要功能:" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:18 #, fuzzy msgid "Customizable work session and break lengths" msgstr "可自定义工作会话和休息时长" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:19 #, fuzzy msgid "Screen overlay during breaks" msgstr "休息时的全屏覆盖层" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:20 #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:95 msgid "System tray icon" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:21 msgid "Hotkeys (global shortcuts)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:22 msgid "Daily, weekly, and monthly statistics" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:23 msgid "Extensible via custom shell commands, D-Bus, and CLI" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:24 msgid "GNOME Shell extension for deeper desktop integration" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:45 msgid "Kamil Prusko" msgstr "Kamil Prusko" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:69 #: src/plugins/sni/indicator-provider.vala:304 src/ui/main/window.ui:76 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:57 #: src/ui/preferences/preferences-window.vala:30 msgid "Timer" msgstr "计时器" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:73 #, fuzzy msgid "Daily stats" msgstr "每日统计" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:77 #, fuzzy msgid "Monthly stats" msgstr "每月统计" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:81 #: src/plugins/sni/indicator-provider.vala:300 #: src/plugins/sni/indicator-provider.vala:306 #: src/ui/preferences/preferences-window.ui:6 msgid "Preferences" msgstr "首选项" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:85 #, fuzzy msgid "Screen overlay" msgstr "全屏覆盖层" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:93 msgid "Overview of changes in focus-timer 1.1.1:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:96 msgid "Smoother sound transitions" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:97 msgid "Fix break overlay scaling on HiDPI displays" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:98 msgid "Fix missing sounds after switching soundcards" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:104 msgid "Overview of changes in focus-timer 1.1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:106 msgid "Support for GNOME Shell extension" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:107 msgid "Option to autostart on login" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:108 msgid "Reviewed sound files" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:109 msgid "Fix build with vala 0.56.19" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:115 msgid "Overview of changes in focus-timer 1.0:" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:117 msgid "Fix break overlay scaling on HiDPI displays (thanks @scholzri)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:118 msgid "Automatic daily backup" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:119 msgid "Removed libcanberra backend for playing notification sounds" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:120 msgid "Updated Lithuanian translation (thanks @psukys)" msgstr "" #: data/io.github.focustimerhq.FocusTimer.metainfo.xml.in:121 msgid "Updated Russian translation (thanks @ViktorOn)" msgstr "" #: src/application.vala:155 src/application.vala:609 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:60 #, fuzzy msgid "Start or Stop" msgstr "开始或停止" #: src/application.vala:158 src/application.vala:612 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:67 #, fuzzy msgid "Start, Pause or Resume" msgstr "开始、暂停或继续" #: src/application.vala:161 src/core/notification-manager.vala:426 #: src/core/notification-manager.vala:519 #: src/plugins/sni/indicator-provider.vala:403 #: src/ui/main/timer/widgets/timer-control-buttons.ui:69 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Start Pomodoro" msgstr "启动番茄计时器" #: src/application.vala:164 msgid "Start break" msgstr "" #: src/application.vala:167 msgid "Start short break" msgstr "" #: src/application.vala:170 msgid "Start long break" msgstr "" #. Actions #: src/application.vala:173 src/application.vala:614 src/core/event.vala:268 #: src/plugins/sni/indicator-provider.vala:250 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:77 msgid "Start" msgstr "开始" #: src/application.vala:176 src/application.vala:616 src/core/event.vala:276 #: src/plugins/sni/indicator-provider.vala:262 #: src/ui/main/timer/widgets/timer-control-buttons.ui:139 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:84 msgid "Stop" msgstr "停止" #: src/application.vala:179 src/application.vala:618 src/core/event.vala:284 #: src/plugins/sni/indicator-provider.vala:253 #: src/ui/main/timer/widgets/timer-control-buttons.ui:83 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:91 msgid "Pause" msgstr "暂停" #: src/application.vala:182 src/application.vala:620 src/core/event.vala:292 #: src/plugins/sni/indicator-provider.vala:256 #: src/ui/main/timer/widgets/timer-control-buttons.ui:97 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:98 msgid "Resume" msgstr "继续" #: src/application.vala:185 src/application.vala:622 src/core/event.vala:300 #: src/plugins/sni/indicator-provider.vala:265 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:105 msgid "Skip" msgstr "跳过" #: src/application.vala:188 src/application.vala:624 src/core/event.vala:308 #, fuzzy msgid "Rewind" msgstr "快退" #: src/application.vala:189 src/application.vala:192 msgid "SECONDS" msgstr "" #: src/application.vala:191 msgid "Extend current pomodoro or break" msgstr "延长当前番茄钟或休息" #: src/application.vala:194 src/core/event.vala:316 #: src/plugins/sni/indicator-provider.vala:268 #: src/ui/main/timer/widgets/timer-control-buttons.ui:39 #, fuzzy msgid "Reset" msgstr "重置" #: src/application.vala:197 msgid "Print timer status" msgstr "" #: src/application.vala:200 msgid "Show preferences" msgstr "显示首选项" #: src/application.vala:203 msgid "Quit application" msgstr "退出应用" #: src/application.vala:206 msgid "Print version information and exit" msgstr "打印版本信息并退出" #: src/application.vala:240 msgid "Timer Options:" msgstr "" #: src/application.vala:241 msgid "Show options for controlling the timer" msgstr "" #: src/application.vala:248 #, c-format msgid "Bugs may be reported at: %s" msgstr "" #: src/application.vala:626 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:126 #, fuzzy msgid "Bring to Focus" msgstr "聚焦窗口" #. translators: time remaining eg. "3 minutes 50 seconds remaining" #: src/application.vala:817 src/core/notification-manager.vala:138 #: src/plugins/sni/indicator-provider.vala:25 #, fuzzy, c-format msgid "%s remaining" msgstr "剩余 %s" #: src/application.vala:860 msgid "Invalid use. Pass one flag for controlling the timer at a time." msgstr "" #: src/core/action-manager.vala:113 #, fuzzy, c-format msgid "Custom action \"%s\" has failed" msgstr "自定义动作“%s”失败" #: src/core/command.vala:379 #, fuzzy msgid "Reached timeout" msgstr "已超时" #: src/core/command.vala:408 #, fuzzy msgid "Failed to execute command" msgstr "无法执行命令" #: src/core/command.vala:491 src/core/command.vala:506 #, fuzzy msgid "Command is empty" msgstr "命令为空" #: src/core/command.vala:510 #, fuzzy msgid "Unclosed quotation mark" msgstr "引号未闭合" #: src/core/command.vala:515 #, fuzzy msgid "Invalid command" msgstr "无效命令" #: src/core/command.vala:540 src/core/expression.vala:859 #, fuzzy, c-format msgid "Unknown variable \"%s\"" msgstr "未知变量“%s”" #: src/core/command.vala:546 src/core/expression.vala:236 #, fuzzy, c-format msgid "Unknown format \"%s\"" msgstr "未知格式“%s”" #: src/core/command.vala:619 #, fuzzy, c-format msgid "Program \"%s\" not found" msgstr "未找到程序“%s”" #: src/core/event.vala:180 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:74 msgid "Actions" msgstr "动作集" #: src/core/event.vala:183 #, fuzzy msgid "Countdown" msgstr "倒计时" #: src/core/event.vala:186 #: src/ui/preferences/timer/preferences-panel-timer.ui:17 #, fuzzy msgid "Session" msgstr "会话" #: src/core/event.vala:189 #, fuzzy msgid "Other" msgstr "其他" #: src/core/event.vala:269 #, fuzzy msgid "Started the timer." msgstr "已启动计时器。" #: src/core/event.vala:277 #, fuzzy msgid "Stopped the timer manually." msgstr "手动停止了计时器。" #: src/core/event.vala:285 #, fuzzy msgid "" "The countdown has been manually paused. Not triggered when locking the " "screen or when suspending the system." msgstr "倒计时已手动暂停。锁定屏幕或挂起系统时不会触发。" #: src/core/event.vala:293 #, fuzzy msgid "The countdown has been manually resumed." msgstr "倒计时已手动恢复。" #: src/core/event.vala:301 #, fuzzy msgid "Jumped to a next time-block before the countdown has finished." msgstr "在倒计时结束前跳转到了下一个时间块。" #: src/core/event.vala:309 #, fuzzy msgid "Rewind action has been used. It adds a pause in the past." msgstr "已使用快退动作。它在过去添加了一段暂停。" #: src/core/event.vala:317 #, fuzzy msgid "Manually cleared the session." msgstr "手动清除了会话。" #. Countdown #: src/core/event.vala:325 #: src/ui/preferences/automation/action/action-edit-window.ui:20 #: src/ui/preferences/automation/action/condition-widget.vala:120 #, fuzzy msgid "Finished" msgstr "已完成" #: src/core/event.vala:326 #, fuzzy msgid "" "The countdown has finished. If waiting for confirmation, the duration of the " "time-block still may be altered." msgstr "倒计时已结束。如果正在等待确认,时间块的持续时间仍可能被修改。" #: src/core/event.vala:333 #, fuzzy msgid "Changed" msgstr "已更改" #: src/core/event.vala:334 #, fuzzy msgid "Triggered on any change related to the countdown." msgstr "在任何与倒计时相关的更改时触发。" #. Session #: src/core/event.vala:342 #, fuzzy msgid "Confirm Advancement" msgstr "确认进度" #: src/core/event.vala:343 #, fuzzy msgid "A manual confirmation is required to start next time-block." msgstr "需要手动确认才能开始下一个时间块。" #: src/core/event.vala:350 #, fuzzy msgid "Advanced" msgstr "已前进" #: src/core/event.vala:351 #, fuzzy msgid "Transitioned or skipped to a next time-block." msgstr "已转换或跳过到下一个时间块。" #: src/core/event.vala:358 #, fuzzy msgid "State Changed" msgstr "状态已改变" #: src/core/event.vala:359 #, fuzzy msgid "Transitioned to a next time-block or when a break gets relabelled." msgstr "转换到下一个时间块或休息被重新标记时。" #: src/core/event.vala:366 #, fuzzy msgid "Rescheduled" msgstr "已重新调度" #. translators: Change of plan #: src/core/event.vala:367 #, fuzzy msgid "Triggered when scheduled time-blocks have changed." msgstr "在预定的时间块发生变化时触发。" #: src/core/event.vala:374 #, fuzzy msgid "Expired" msgstr "已过期" #: src/core/event.vala:375 #, fuzzy msgid "Triggered when session is about to be reset due to inactivity." msgstr "在会话因不活动即将被重置时触发。" #: src/core/notification-manager.vala:347 src/core/state.vala:78 #: src/plugins/sni/indicator-provider.vala:271 #: src/ui/main/stats/stats-day-page.ui:98 #: src/ui/main/stats/stats-day-page.vala:87 #: src/ui/main/stats/stats-month-page.ui:35 #: src/ui/main/stats/stats-month-page.vala:43 #: src/ui/main/stats/stats-week-page.ui:39 #: src/ui/main/stats/stats-week-page.vala:41 src/ui/main/window.vala:186 #: src/ui/preferences/automation/action/condition-widget.ui:53 msgid "Pomodoro" msgstr "番茄计时器" #: src/core/notification-manager.vala:351 #: src/ui/main/timer/widgets/timer-control-buttons.vala:308 #: src/ui/main/timer/widgets/timer-control-buttons.vala:324 msgid "Take a break" msgstr "休息一下吧" #: src/core/notification-manager.vala:355 msgid "Take a short break" msgstr "休息一下" #: src/core/notification-manager.vala:359 msgid "Take a long break" msgstr "休息很久" #: src/core/notification-manager.vala:418 msgid "Pomodoro is about to end" msgstr "番茄钟即将结束" #: src/core/notification-manager.vala:419 #: src/core/notification-manager.vala:524 #: src/core/notification-manager.vala:529 #: src/core/notification-manager.vala:534 src/ui/overlays/screen-overlay.ui:5 #, fuzzy msgid "Take a Break" msgstr "休息一下" #: src/core/notification-manager.vala:425 msgid "Break is about to end" msgstr "休息即将结束" #: src/core/notification-manager.vala:436 #, fuzzy msgid "+1 minute" msgstr "+1 分钟" #: src/core/notification-manager.vala:458 msgid "Get ready…" msgstr "做好准备…" #: src/core/notification-manager.vala:463 #: src/core/notification-manager.vala:502 #, fuzzy msgid "Pomodoro is over!" msgstr "番茄钟结束了!" #: src/core/notification-manager.vala:469 #: src/core/notification-manager.vala:508 #, fuzzy msgid "Break is over!" msgstr "休息结束了!" #: src/core/notification-manager.vala:518 #, fuzzy msgid "Confirm the start of a Pomodoro…" msgstr "确认启动番茄钟…" #: src/core/notification-manager.vala:523 #, fuzzy msgid "Confirm the start of a break…" msgstr "确认开始休息…" #: src/core/notification-manager.vala:528 #, fuzzy msgid "Confirm the start of a short break…" msgstr "确认开始短休息…" #: src/core/notification-manager.vala:533 #, fuzzy msgid "Confirm the start of a long break…" msgstr "确认开始长休息…" #: src/core/notification-manager.vala:546 msgid "Skip Break" msgstr "跳过休息" #: src/core/sound-player.vala:101 #, fuzzy msgid "Failed to initialize playback" msgstr "播放初始化失败" #: src/core/sounds.vala:112 #, fuzzy msgid "File not found" msgstr "未找到文件" #: src/core/sounds.vala:116 #, fuzzy msgid "File type not supported" msgstr "文件类型不支持" #: src/core/state.vala:75 #: src/ui/preferences/automation/action/condition-widget.ui:55 #, fuzzy msgid "Stopped" msgstr "已停止" #: src/core/state.vala:81 src/plugins/sni/indicator-provider.vala:274 #: src/ui/preferences/automation/action/condition-widget.ui:54 msgid "Break" msgstr "休息" #: src/core/state.vala:84 src/plugins/sni/indicator-provider.vala:272 msgid "Short Break" msgstr "短休息" #: src/core/state.vala:87 src/plugins/sni/indicator-provider.vala:273 msgid "Long Break" msgstr "长休息" #. translators: Short form for number of hours #: src/core/timestamp.vala:117 #, fuzzy, c-format msgid "%uh" msgstr "%u小时" #. translators: Short form for number of minutes #: src/core/timestamp.vala:126 #, fuzzy, c-format msgid "%um" msgstr "%u分钟" #: src/core/utils.vala:72 #, fuzzy, c-format msgid "%u hour" msgid_plural "%u hours" msgstr[0] "%u 小时" #: src/core/utils.vala:81 #, fuzzy, c-format msgid "%u minute" msgid_plural "%u minutes" msgstr[0] "%u 分钟" #: src/core/utils.vala:90 #, fuzzy, c-format msgid "%u second" msgid_plural "%u seconds" msgstr[0] "%u 秒" #: src/core/variables.vala:116 #, fuzzy msgid "The exact time of the current event." msgstr "当前事件的精确时间。" #: src/core/variables.vala:121 #, fuzzy msgid "" "The current phase of the Pomodoro cycle. Possible values: stopped, " "pomodoro, break, short-break, long-break." msgstr "" "番茄周期的当前阶段。可选值:stopped(停止)、pomodoro(番" "茄)、break(休息)、short-break(短休息)、long-" "break(长休息)。" #: src/core/variables.vala:126 #, fuzzy msgid "" "Status of the current time-block. Possible values: scheduled, " "in-progress, completed, uncompleted." msgstr "" "当前时间块的状态。可选值:scheduled(已预定)、in-progress" "(进行中)、completed(已完成)、uncompleted(未完成)。" #: src/core/variables.vala:131 #, fuzzy msgid "A flag indicating whether countdown has begun." msgstr "指示倒计时是否已开始的标志。" #: src/core/variables.vala:136 #, fuzzy msgid "A flag indicating whether countdown is paused." msgstr "指示倒计时是否已暂停的标志。" #: src/core/variables.vala:141 #, fuzzy msgid "A flag indicating whether countdown has finished." msgstr "指示倒计时是否已结束的标志。" #: src/core/variables.vala:146 #, fuzzy msgid "A flag indicating whether the timer is actively counting down." msgstr "指示计时器是否正在计数的标志。" #: src/core/variables.vala:151 #, fuzzy msgid "Duration of the current countdown." msgstr "当前倒计时的持续时间。" #. translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. #: src/core/variables.vala:157 #, fuzzy msgid "Discrepancy between elapsed time and the time passed." msgstr "计时器显示时间与实际流逝时间之间的差异。" #. translators: Time since the start of countdown #: src/core/variables.vala:163 #, fuzzy msgid "The amount of time spent on the countdown." msgstr "倒计时已耗费的时间。" #. translators: Displayed timer value. #: src/core/variables.vala:169 #, fuzzy msgid "The amount of time left before the countdown ends." msgstr "倒计时结束前的剩余时间。" #: src/core/variables.vala:174 #, fuzzy msgid "Time when the countdown has started." msgstr "倒计时开始的时间。" #: src/plugins/gnome/install-extension-dialog.ui:6 #: src/plugins/gnome/preferences-window-extension.vala:167 #, fuzzy msgid "GNOME Shell Extension" msgstr "GNOME Shell 扩展" #: src/plugins/gnome/install-extension-dialog.ui:57 #, fuzzy msgid "Get the best experience!" msgstr "获得最佳体验!" #: src/plugins/gnome/install-extension-dialog.ui:68 #, fuzzy msgid "Enable GNOME Shell extension for seamless desktop integration" msgstr "启用 GNOME Shell 扩展 以获得无缝桌面集成" #: src/plugins/gnome/install-extension-dialog.ui:95 #, fuzzy msgid "Always within reach" msgstr "触手可及" #: src/plugins/gnome/install-extension-dialog.ui:106 #, fuzzy msgid "Control timer directly from the top bar without opening the app" msgstr "无需打开应用即可直接从顶栏控制计时器" #: src/plugins/gnome/install-extension-dialog.ui:132 #, fuzzy msgid "Less distractions" msgstr "减少干扰" #: src/plugins/gnome/install-extension-dialog.ui:143 msgid "" "Let Focus Timer manage system notifications while the timer is running" msgstr "" #: src/plugins/gnome/install-extension-dialog.ui:170 #, fuzzy msgid "Refined break reminders" msgstr "精致的休息提醒" #: src/plugins/gnome/install-extension-dialog.ui:181 #, fuzzy msgid "" "Elegant full-screen overlay that make taking breaks a more pleasant " "experience" msgstr "优雅的全屏覆盖层,让休息体验更愉快" #. translators: "It" refers to installing GNOME Shell extension #: src/plugins/gnome/install-extension-dialog.ui:195 #, fuzzy msgid "Ready to try it?" msgstr "准备好尝试了吗?" #: src/plugins/gnome/install-extension-dialog.ui:219 #: src/plugins/gnome/install-extension-dialog.ui:252 #, fuzzy msgid "_Install Extension" msgstr "安装扩展(_I)" #: src/plugins/gnome/install-extension-dialog.ui:229 #: src/plugins/gnome/install-extension-dialog.ui:245 #, fuzzy msgid "_Not Now" msgstr "以后再说(_N)" #: src/plugins/gnome/install-extension-dialog.ui:317 #: src/plugins/gnome/preferences-window-extension.vala:400 #, fuzzy msgid "Something went wrong" msgstr "出了点问题" #: src/plugins/gnome/install-extension-dialog.ui:364 #, fuzzy msgid "Copy to clipboard" msgstr "复制到剪贴板" #: src/plugins/gnome/install-extension-dialog.ui:383 #: src/plugins/gnome/install-extension-dialog.ui:416 #, fuzzy msgid "_Try Again" msgstr "重试(_T)" #: src/plugins/gnome/install-extension-dialog.ui:393 #: src/plugins/gnome/install-extension-dialog.ui:409 #, fuzzy msgid "_Abort" msgstr "中止(_A)" #: src/plugins/gnome/install-extension-dialog.vala:85 #: src/plugins/gnome/preferences-window-extension.vala:388 #, fuzzy msgid "Time-out reached" msgstr "已超时" #: src/plugins/gnome/install-extension-dialog.vala:90 #: src/plugins/gnome/preferences-window-extension.vala:392 #, fuzzy msgid "Installing extensions is not allowed" msgstr "不允许安装扩展" #: src/plugins/gnome/install-extension-dialog.vala:95 #: src/plugins/gnome/preferences-window-extension.vala:396 #, fuzzy msgid "Failed to download the extension" msgstr "无法下载扩展" #: src/plugins/gnome/preferences-window-extension.vala:55 msgid "Indicator" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:62 msgid "Icon" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:63 msgid "Text" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:70 msgid "Display As" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:81 #: src/plugins/sni/indicator-provider.vala:297 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:28 #: src/ui/preferences/notifications/preferences-panel-notifications.ui:32 #, fuzzy msgid "Screen Overlay" msgstr "全屏覆盖层" #: src/plugins/gnome/preferences-window-extension.vala:85 msgid "Blur Effect" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:93 msgid "Dismiss Gesture" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:125 msgid "Desktop" msgstr "桌面" #: src/plugins/gnome/preferences-window-extension.vala:128 msgid "Install" msgstr "" #. translators: verb #: src/plugins/gnome/preferences-window-extension.vala:133 msgid "Update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:136 msgid "Log out to finish the update" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:154 msgid "Outdated" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:173 msgid "Manage Notifications" msgstr "" #: src/plugins/gnome/preferences-window-extension.vala:174 msgid "Toggle Do Not Disturb mode during Pomodoro." msgstr "" #: src/plugins/gnome/window-extension.vala:32 #, fuzzy msgid "GNOME Shell extension available" msgstr "GNOME Shell 扩展可用" #: src/plugins/gnome/window-extension.vala:33 #, fuzzy msgid "Learn More" msgstr "了解更多" #. translators: abbreviate it to just "Settings" if it gets too long #: src/plugins/kde/preferences-window-extension.vala:26 msgid "Open Settings" msgstr "" #: src/plugins/kde/preferences-window-extension.vala:51 msgid "" "For reliable break reminders, allow this app's notifications during Do Not " "Disturb and disable its notification history." msgstr "" #: src/plugins/portal/global-shortcuts-provider.vala:298 #, fuzzy msgid "Unused" msgstr "未使用" #: src/plugins/sni/indicator-provider.vala:35 #: src/ui/main/timer/compact-timer-view.vala:73 #: src/ui/main/timer/timer-view.vala:141 #, fuzzy msgid "Finished!" msgstr "已完成!" #: src/plugins/sni/indicator-provider.vala:42 #, c-format msgid "%u of %u" msgstr "" #: src/plugins/sni/indicator-provider.vala:301 #: src/plugins/sni/indicator-provider.vala:305 src/ui/main/window.ui:92 msgid "Stats" msgstr "统计" #: src/plugins/sni/indicator-provider.vala:310 src/ui/main/window.vala:284 msgid "Quit" msgstr "退出" #: src/plugins/sni/indicator-provider.vala:403 msgid "Take Break" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:43 msgid "System Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:47 msgid "Show Tray Icon" msgstr "" #: src/plugins/sni/preferences-window-extension.vala:48 msgid "Closing the window keeps the app running in the background." msgstr "" #: src/ui/log/log-window.ui:6 #, fuzzy msgid "Log" msgstr "日志" #: src/ui/log/log-window.ui:37 #, fuzzy msgid "Empty Log" msgstr "空日志" #: src/ui/log/log-window.ui:38 #, fuzzy msgid "Entries will show up here once you start the timer." msgstr "启动计时器后,条目将显示在此处。" #: src/ui/log/log-window.ui:164 #, fuzzy msgid "Context" msgstr "上下文" #: src/ui/log/log-window.ui:189 #: src/ui/preferences/automation/action/action-edit-window.ui:186 msgid "Command" msgstr "命令" #: src/ui/log/log-window.ui:213 #, fuzzy msgid "Output" msgstr "输出" #: src/ui/log/log-window.ui:237 #, fuzzy msgid "Error" msgstr "错误" #: src/ui/log/log-window.ui:266 #, fuzzy msgid "Exit Code:" msgstr "退出代码:" #: src/ui/log/log-window.ui:277 #, fuzzy msgid "Execution Time:" msgstr "执行时间:" #. translators: Replace this string with your names, one name per line. #: src/ui/main/dialogs/about-dialog.vala:30 msgid "translator-credits" msgstr "" "Meng Zhuo , 2012\n" "soiamsoNG <83182235@qq.com>, 2017\n" "wffger , 2020" #: src/ui/main/dialogs/about-dialog.vala:36 #, fuzzy msgid "Donate" msgstr "捐赠" #: src/ui/main/stats/stats-day-page.ui:105 #: src/ui/main/stats/stats-day-page.vala:92 #: src/ui/main/stats/stats-month-page.ui:42 #: src/ui/main/stats/stats-month-page.vala:48 #: src/ui/main/stats/stats-week-page.ui:46 #: src/ui/main/stats/stats-week-page.vala:46 #, fuzzy msgid "Breaks" msgstr "休息次数" #: src/ui/main/stats/stats-day-page.ui:112 #: src/ui/main/stats/stats-month-page.ui:49 #: src/ui/main/stats/stats-month-page.vala:53 #: src/ui/main/stats/stats-week-page.ui:53 #: src/ui/main/stats/stats-week-page.vala:51 #, fuzzy msgid "Interruptions" msgstr "干扰" #: src/ui/main/stats/stats-day-page.ui:119 #: src/ui/main/stats/stats-month-page.ui:56 #: src/ui/main/stats/stats-week-page.ui:60 #, fuzzy msgid "Break Ratio" msgstr "休息比例" #: src/ui/main/stats/stats-view.ui:8 src/ui/main/stats/stats-view.vala:59 #: src/ui/main/stats/widgets/stats-date-popover.ui:22 msgid "Day" msgstr "天" #: src/ui/main/stats/stats-view.ui:13 src/ui/main/stats/stats-view.vala:62 #: src/ui/main/stats/widgets/stats-date-popover.ui:28 msgid "Week" msgstr "周" #: src/ui/main/stats/stats-view.ui:18 src/ui/main/stats/stats-view.vala:65 #: src/ui/main/stats/widgets/stats-date-popover.ui:34 msgid "Month" msgstr "月" #: src/ui/main/stats/stats-view.ui:39 #, fuzzy msgid "Nothing to see here yet" msgstr "暂无统计信息" #: src/ui/main/stats/stats-view.ui:40 #, fuzzy msgid "Finish a few Pomodoros to fill this up!" msgstr "完成几个番茄钟来填充这里!" #: src/ui/main/stats/stats-view.vala:831 #, fuzzy, c-format msgid "Skipped %u day" msgid_plural "Skipped %u days" msgstr[0] "已跳过 %u 天" #: src/ui/main/stats/stats-view.vala:837 #, fuzzy, c-format msgid "Skipped %u week" msgid_plural "Skipped %u weeks" msgstr[0] "已跳过 %u 周" #: src/ui/main/stats/stats-view.vala:843 #, fuzzy, c-format msgid "Skipped %u month" msgid_plural "Skipped %u months" msgstr[0] "已跳过 %u 个月" #: src/ui/main/stats/stats-view.vala:1041 msgid "Today" msgstr "今天" #: src/ui/main/stats/stats-view.vala:1046 msgid "Yesterday" msgstr "昨天" #: src/ui/main/stats/stats-view.vala:1067 msgid "This week" msgstr "本周" #: src/ui/main/stats/stats-view.vala:1087 #, fuzzy, c-format msgid "Week %u" msgstr "第 %u 周" #: src/ui/main/stats/stats-view.vala:1088 #, fuzzy, c-format msgid "Week %u of %u" msgstr "%2$u 年第 %1$u 周" #: src/ui/main/timer/compact-timer-view.ui:8 src/ui/main/timer/menus.ui:6 #: src/ui/main/timer/menus.ui:22 msgid "_Pomodoro" msgstr "番茄计时器(_P)" #: src/ui/main/timer/compact-timer-view.ui:13 src/ui/main/timer/menus.ui:10 msgid "_Short Break" msgstr "短休息(_S)" #: src/ui/main/timer/compact-timer-view.ui:18 src/ui/main/timer/menus.ui:14 msgid "_Long Break" msgstr "长休息(_L)" #: src/ui/main/timer/menus.ui:26 #, fuzzy msgid "_Break" msgstr "休息(_B)" #: src/ui/main/timer/timer-view.ui:23 #, fuzzy msgid "Open screen overlay" msgstr "打开全屏覆盖层" #: src/ui/main/timer/timer-view.vala:257 #, fuzzy msgid "Session has expired" msgstr "会话已过期" #: src/ui/main/timer/widgets/session-progress-bar.vala:1477 #, fuzzy, c-format msgid "Long break due in %s" msgstr "将在 %s 后进行长休息" #: src/ui/main/timer/widgets/timer-control-buttons.ui:25 #, fuzzy msgid "Rewind one minute" msgstr "快退一分钟" #: src/ui/main/window.ui:8 #, fuzzy msgid "_Compact View" msgstr "紧凑视图(_C)" #: src/ui/main/window.ui:15 msgid "_Preferences" msgstr "首选项(_P)" #: src/ui/main/window.ui:19 msgid "_About" msgstr "关于(_A)" #: src/ui/main/window.ui:25 msgid "_Quit" msgstr "退出(_Q)" #: src/ui/main/window.ui:62 #, fuzzy msgid "Primary Menu" msgstr "主菜单" #: src/ui/main/window.vala:279 #, fuzzy msgid "Keep timer running?" msgstr "保持计时器运行?" #: src/ui/main/window.vala:280 #, fuzzy msgid "" "You can keep it running in the background — notifications and keyboard " "shortcuts will still work." msgstr "您可以让它在后台运行 — 通知和键盘快捷键仍将生效。" #: src/ui/main/window.vala:287 #, fuzzy msgid "Run in background" msgstr "在后台运行" #: src/ui/overlays/screen-overlay.ui:64 msgid "It's time to take a break" msgstr "是时候休息一下了" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:17 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:123 #, fuzzy msgid "Main Window" msgstr "主窗口" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:20 #, fuzzy msgid "Prefer Dark Theme" msgstr "偏好暗色主题" #: src/ui/preferences/appearance/preferences-panel-appearance.ui:25 #, fuzzy msgid "Prefer Compact View" msgstr "偏好紧凑视图" #: src/ui/preferences/automation/action/action-edit-window.ui:12 #: src/ui/preferences/automation/action/condition-widget.vala:117 #, fuzzy msgid "Started" msgstr "已启动" #: src/ui/preferences/automation/action/action-edit-window.ui:16 #: src/ui/preferences/automation/action/condition-widget.vala:118 msgid "Paused" msgstr "已暂停" #: src/ui/preferences/automation/action/action-edit-window.ui:26 #, fuzzy msgid "Edit Custom Action" msgstr "编辑自定义动作" #: src/ui/preferences/automation/action/action-edit-window.ui:39 #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:21 msgid "_Cancel" msgstr "取消(_C)" #: src/ui/preferences/automation/action/action-edit-window.ui:46 #, fuzzy msgid "_Save" msgstr "保存(_S)" #: src/ui/preferences/automation/action/action-edit-window.ui:62 msgid "Name" msgstr "名称" #: src/ui/preferences/automation/action/action-edit-window.ui:76 #, fuzzy msgid "Trigger" msgstr "触发器" #: src/ui/preferences/automation/action/action-edit-window.ui:80 #, fuzzy msgid "Event" msgstr "事件" #: src/ui/preferences/automation/action/action-edit-window.ui:81 #, fuzzy msgid "Execute command after an event." msgstr "事件发生后执行命令。" #: src/ui/preferences/automation/action/action-edit-window.ui:96 #: src/ui/preferences/automation/action/action-edit-window.ui:174 #, fuzzy msgid "Condition" msgstr "条件" #: src/ui/preferences/automation/action/action-edit-window.ui:97 #, fuzzy msgid "Ensure execution of a second command once condition is no longer met." msgstr "确保在条件不再满足时执行第二个命令。" #: src/ui/preferences/automation/action/action-edit-window.ui:114 #, fuzzy msgid "Events" msgstr "事件" #: src/ui/preferences/automation/action/action-edit-window.ui:125 #, fuzzy msgid "Add _Event" msgstr "添加事件(_E)" #. translators: Filter (verb) #: src/ui/preferences/automation/action/action-edit-window.ui:140 #, fuzzy msgid "_Filter" msgstr "过滤(_F)" #. translators: Filter (noun) #: src/ui/preferences/automation/action/action-edit-window.ui:160 #, fuzzy msgid "Filter" msgstr "过滤器" #: src/ui/preferences/automation/action/action-edit-window.ui:191 #, fuzzy msgid "Shell Command" msgstr "Shell 命令" #: src/ui/preferences/automation/action/action-edit-window.ui:199 #, fuzzy msgid "Commands" msgstr "命令" #: src/ui/preferences/automation/action/action-edit-window.ui:204 #, fuzzy msgid "Condition Met Command" msgstr "满足条件时的命令" #: src/ui/preferences/automation/action/action-edit-window.ui:210 #, fuzzy msgid "Condition Not Met Command" msgstr "未满足条件时的命令" #: src/ui/preferences/automation/action/action-edit-window.ui:221 #, fuzzy msgid "Working Directory" msgstr "工作目录" #: src/ui/preferences/automation/action/action-edit-window.ui:236 #, fuzzy msgid "Use Subshell" msgstr "使用子 Shell" #: src/ui/preferences/automation/action/action-edit-window.ui:237 #, fuzzy msgid "Run the program from a subshell such as sh -c ''" msgstr "从子 Shell 运行程序,例如 sh -c ''" #: src/ui/preferences/automation/action/action-edit-window.ui:242 #, fuzzy msgid "Pass Input Data" msgstr "传递输入数据" #: src/ui/preferences/automation/action/action-edit-window.ui:243 #, fuzzy msgid "Instead of passing variables you can process a JSON object." msgstr "您可以处理 JSON 对象,而不是传递变量。" #: src/ui/preferences/automation/action/action-edit-window.ui:248 #, fuzzy msgid "Wait For Completion" msgstr "等待完成" #: src/ui/preferences/automation/action/action-edit-window.ui:249 #, fuzzy msgid "Block execution of other commands until the command completes." msgstr "阻塞其他命令的执行,直到此命令完成。" #: src/ui/preferences/automation/action/action-edit-window.ui:259 #, fuzzy msgid "_Delete Action" msgstr "删除动作(_D)" #: src/ui/preferences/automation/action/action-edit-window.vala:230 #, fuzzy msgid "No events specified yet." msgstr "尚未指定事件。" #: src/ui/preferences/automation/action/action-edit-window.vala:248 #, fuzzy msgid "Add Custom Action" msgstr "添加自定义动作" #: src/ui/preferences/automation/action/action-edit-window.vala:249 #, fuzzy msgid "_Add" msgstr "添加(_A)" #: src/ui/preferences/automation/action/action-edit-window.vala:438 #, fuzzy msgid "Select Working Directory" msgstr "选择工作目录" #: src/ui/preferences/automation/action/action-edit-window.vala:440 #: src/ui/preferences/sounds/sound-chooser-window.vala:251 msgid "_Select" msgstr "选择(_S)" #: src/ui/preferences/automation/action/action-listboxrow.vala:67 #, fuzzy msgid "Untitled action" msgstr "未命名动作" #: src/ui/preferences/automation/action/condition-group-widget.ui:28 #, fuzzy msgid "Add Condition" msgstr "添加条件" #: src/ui/preferences/automation/action/condition-group-widget.ui:45 #, fuzzy msgid "Add Group" msgstr "添加组" #: src/ui/preferences/automation/action/condition-group-widget.vala:344 msgid "AND" msgstr "且" #: src/ui/preferences/automation/action/condition-group-widget.vala:345 msgid "OR" msgstr "或" #: src/ui/preferences/automation/action/condition-widget.ui:26 #, fuzzy msgid "Is" msgstr "是" #: src/ui/preferences/automation/action/condition-widget.ui:27 #, fuzzy msgid "Is Not" msgstr "不是" #: src/ui/preferences/automation/action/condition-widget.ui:39 #, fuzzy msgid "Equals" msgstr "等于" #: src/ui/preferences/automation/action/condition-widget.ui:40 #, fuzzy msgid "Greater Than" msgstr "大于" #: src/ui/preferences/automation/action/condition-widget.ui:41 #, fuzzy msgid "Less Than" msgstr "小于" #: src/ui/preferences/automation/action/condition-widget.ui:67 #, fuzzy msgid "Yes" msgstr "是" #: src/ui/preferences/automation/action/condition-widget.ui:68 #, fuzzy msgid "No" msgstr "否" #: src/ui/preferences/automation/action/condition-widget.ui:95 #, fuzzy msgid "Minutes" msgstr "分钟" #: src/ui/preferences/automation/action/condition-widget.ui:96 #, fuzzy msgid "Seconds" msgstr "秒" #: src/ui/preferences/automation/action/condition-widget.ui:97 #, fuzzy msgid "Hours" msgstr "小时" #. translators: No field selected when defining a condition. #: src/ui/preferences/automation/action/condition-widget.vala:115 #, fuzzy msgid "Select Field…" msgstr "选择字段…" #: src/ui/preferences/automation/action/condition-widget.vala:116 msgid "State" msgstr "状态" #: src/ui/preferences/automation/action/condition-widget.vala:119 #, fuzzy msgid "Running" msgstr "运行中" #: src/ui/preferences/automation/action/condition-widget.vala:121 #, fuzzy msgid "Duration" msgstr "时长" #: src/ui/preferences/automation/action/variable-popover.ui:19 #: src/ui/preferences/automation/action/variable-popover.ui:161 #, fuzzy msgid "Insert Variable" msgstr "插入变量" #: src/ui/preferences/automation/action/variable-popover.ui:132 #, fuzzy msgid "Format" msgstr "格式" #: src/ui/preferences/automation/preferences-panel-automation.ui:13 #, fuzzy msgid "_Log" msgstr "日志(_L)" #: src/ui/preferences/automation/preferences-panel-automation.ui:15 #, fuzzy msgid "Show execution log" msgstr "显示执行日志" #: src/ui/preferences/automation/preferences-panel-automation.ui:24 #, fuzzy msgid "" "Run shell commands automatically on timer events or conditions. Learn more." msgstr "" "在计时器事件或满足条件时自动运行 Shell 命令。了解更多。" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:19 msgid "Autostart" msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:20 msgid "Automatically launch the app when you log in." msgstr "" #: src/ui/preferences/integrations/preferences-panel-integrations.ui:25 msgid "" "The app will start in the background. You'll be able to use the indicator " "and keyboard shortcuts." msgstr "" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:14 #, fuzzy msgid "Set Shortcut" msgstr "设置快捷键" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:28 #, fuzzy msgid "_Set" msgstr "设置(_S)" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:63 #: src/ui/preferences/keyboard-shortcuts/accelerator-row.vala:50 #, fuzzy msgid "Disabled" msgstr "已禁用" #: src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui:77 #, fuzzy msgid "" "Press Esc to cancel or Backspace to disable the keyboard " "shortcut" msgstr "按 Esc 取消,或按 Backspace 禁用快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:21 #, fuzzy msgid "" "Global shortcuts let you control the app even when it’s not on screen. They " "work as long as the app is running in the background." msgstr "" "全局快捷键让您即使在应用不在屏幕上时也能控制它。只要应用在后台运行,它们就会" "生效。" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:24 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:40 #, fuzzy msgid "Open app settings for editing global shortcuts" msgstr "打开应用设置以编辑全局快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:29 #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:45 #, fuzzy msgid "_Edit" msgstr "编辑(_E)" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:61 #, fuzzy msgid "Enter new shortcut for starting or stopping the timer" msgstr "输入用于启动或停止计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:68 #, fuzzy msgid "Enter new shortcut to start/pause/resume the timer" msgstr "输入用于开始/暂停/恢复计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:78 #, fuzzy msgid "Enter new shortcut for starting the timer" msgstr "输入用于启动计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:85 #, fuzzy msgid "Enter new shortcut for stopping the timer" msgstr "输入用于停止计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:92 #, fuzzy msgid "Enter new shortcut for pausing the timer" msgstr "输入用于暂停计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:99 #, fuzzy msgid "Enter new shortcut for resuming the timer" msgstr "输入用于恢复计时器的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:106 #, fuzzy msgid "Enter new shortcut for skipping" msgstr "输入用于跳过的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:112 #, fuzzy msgid "Rewind One Minute" msgstr "快退一分钟" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:113 #, fuzzy msgid "Enter new shortcut for rewinding" msgstr "输入用于快退的新快捷键" #: src/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui:127 #, fuzzy msgid "Enter new shortcut for bringing window to focus" msgstr "输入用于将窗口聚焦的新快捷键" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:17 #, fuzzy msgid "Announcements" msgstr "公告" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:20 #, fuzzy msgid "Time Running Out" msgstr "时间即将耗尽" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:21 #, fuzzy msgid "Notify when Pomodoro or break is about to end." msgstr "番茄钟或休息即将结束时发出通知。" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:29 #, fuzzy msgid "A full-screen notification intended to enforce taking a break." msgstr "旨在强制休息的全屏通知。" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:37 #, fuzzy msgid "Lock Delay" msgstr "锁定延迟" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:38 #, fuzzy msgid "Period of inactivity to lock the screen." msgstr "锁定屏幕前的不活动时间。" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:43 #, fuzzy msgid "Reopen Delay" msgstr "重新打开延迟" #: src/ui/preferences/notifications/preferences-panel-notifications.ui:44 #, fuzzy msgid "Period of inactivity to reopen the overlay after it gets dismissed." msgstr "覆盖层关闭后,重新打开前的不活动时间。" #: src/ui/preferences/notifications/preferences-panel-notifications.vala:97 #, fuzzy msgid "Never" msgstr "从不" #: src/ui/preferences/preferences-window.vala:37 msgid "Notifications" msgstr "通知" #: src/ui/preferences/preferences-window.vala:44 #, fuzzy msgid "Sounds" msgstr "声音" #: src/ui/preferences/preferences-window.vala:51 #, fuzzy msgid "Appearance" msgstr "外观" #: src/ui/preferences/preferences-window.vala:58 #, fuzzy msgid "Keyboard Shortcuts" msgstr "键盘快捷键" #: src/ui/preferences/preferences-window.vala:71 msgid "Integrations" msgstr "" #: src/ui/preferences/preferences-window.vala:79 #, fuzzy msgid "Automation" msgstr "自动化" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:24 #, fuzzy msgid "Sounds Are Disabled" msgstr "声音已禁用" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:36 #, fuzzy msgid "Alert Sounds" msgstr "警报声音" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:39 #, fuzzy msgid "Pomodoro Finished Sound" msgstr "番茄钟完成后的声音" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:64 #, fuzzy msgid "Break Finished Sound" msgstr "休息结束后的声音" #: src/ui/preferences/sounds/preferences-panel-sounds.ui:91 #: src/ui/preferences/sounds/preferences-panel-sounds.ui:94 #, fuzzy msgid "Background Sound" msgstr "背景声音" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:20 msgid "Bell" msgstr "铃声" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:21 msgid "Loud Bell" msgstr "响亮铃声" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:24 msgid "Clock Ticking" msgstr "时钟滴答" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:25 msgid "Metronome" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:26 msgid "Brown Noise" msgstr "" #: src/ui/preferences/sounds/preferences-panel-sounds.vala:88 #: src/ui/preferences/sounds/sound-chooser-window.ui:25 #, fuzzy msgid "None" msgstr "无" #: src/ui/preferences/sounds/sound-chooser-window.ui:63 msgid "Volume:" msgstr "音量:" #: src/ui/preferences/sounds/sound-chooser-window.vala:249 msgid "Select Custom Sound" msgstr "选择自定义声音" #: src/ui/preferences/timer/preferences-panel-timer.ui:20 #, fuzzy msgid "Pomodoro Duration" msgstr "番茄钟时长" #: src/ui/preferences/timer/preferences-panel-timer.ui:31 #, fuzzy msgid "Short Break Duration" msgstr "短休息时长" #: src/ui/preferences/timer/preferences-panel-timer.ui:42 #, fuzzy msgid "Long Break Duration" msgstr "长休息时长" #: src/ui/preferences/timer/preferences-panel-timer.ui:53 #, fuzzy msgid "Number of Cycles" msgstr "循环周期数" #: src/ui/preferences/timer/preferences-panel-timer.ui:93 #, fuzzy msgid "Behavior" msgstr "行为" #: src/ui/preferences/timer/preferences-panel-timer.ui:96 #, fuzzy msgid "Pause By Locking The Screen" msgstr "锁定屏幕时暂停" #: src/ui/preferences/timer/preferences-panel-timer.ui:101 #, fuzzy msgid "Confirm Starting a Break" msgstr "开始休息前需确认" #: src/ui/preferences/timer/preferences-panel-timer.ui:106 #, fuzzy msgid "Confirm Starting a Pomodoro" msgstr "开始番茄钟前需确认" #. translators: time formatted as text: "5 minutes 30 seconds" #: src/ui/preferences/timer/preferences-panel-timer.vala:96 #, fuzzy, c-format msgid "A single session will take %s." msgstr "单个会话将耗时 %s。" #: src/ui/preferences/timer/preferences-panel-timer.vala:97 #, fuzzy, c-format msgid "%u%% of the time will be allocated for breaks." msgstr "%u%% 的时间将分配给休息。" #: src/ui/preferences/timer/preferences-panel-timer.vala:144 #, fuzzy msgid "Apply changes to ongoing Pomodoro?" msgstr "应用更改到正在进行的番茄钟?" #: src/ui/preferences/timer/preferences-panel-timer.vala:145 #, fuzzy msgid "Apply changes to ongoing break?" msgstr "应用更改到正在进行的休息?" #: src/ui/preferences/timer/preferences-panel-timer.vala:147 #, fuzzy msgid "Apply" msgstr "应用" #: src/ui/preferences/widgets/preferences-sidebar.vala:73 msgctxt "accessibility" msgid "Sidebar" msgstr "侧边栏" #, fuzzy #~ msgid "Time management utility" #~ msgstr "时间管理工具" #, fuzzy #~ msgid "Maintain focus by taking frequent breaks" #~ msgstr "通过经常休息来保持专注" #, fuzzy #~ msgid "Visual and audio notifications" #~ msgstr "视觉与音频通知" #, fuzzy #~ msgid "Time tracking and statistics" #~ msgstr "时间追踪与统计" #, fuzzy #~ msgid "GNOME desktop integration" #~ msgstr "GNOME 桌面集成" #, fuzzy #~ msgid "Run custom commands after Pomodoro or break" #~ msgstr "在番茄钟或休息结束后运行自定义命令" #, fuzzy #~ msgid "Compact timer" #~ msgstr "紧凑计时器" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.1" #~ msgstr "gnome-pomodoro 0.28.1 更新概览" #, fuzzy #~ msgid "Added Tamil translation (thanks @omeritzics)" #~ msgstr "添加了泰米尔语翻译(感谢 @omeritzics)" #, fuzzy #~ msgid "Added Hebrew translation (thanks @Killersparrow1)" #~ msgstr "添加了希伯来语翻译(感谢 @Killersparrow1)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.28.0" #~ msgstr "gnome-pomodoro 0.28.0 更新概览" #, fuzzy #~ msgid "Support for GNOME Shell 49 (thanks @aleasto)" #~ msgstr "支持 GNOME Shell 49(感谢 @aleasto)" #, fuzzy #~ msgid "Updated German translation (thanks @daPhipz)" #~ msgstr "更新了德语翻译(感谢 @daPhipz)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.27.0" #~ msgstr "gnome-pomodoro 0.27.0 更新概览" #, fuzzy #~ msgid "Support for GNOME Shell 48" #~ msgstr "支持 GNOME Shell 48" #, fuzzy #~ msgid "Split time spent across midnight" #~ msgstr "跨午夜时段的时间分割" #, fuzzy #~ msgid "Added Telugu translation (thanks @SpaciousCoder78)" #~ msgstr "添加了泰卢固语翻译(感谢 @SpaciousCoder78)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.26.0" #~ msgstr "gnome-pomodoro 0.26.0 更新概览" #, fuzzy #~ msgid "Support for GNOME Shell 47" #~ msgstr "支持 GNOME Shell 47" #, fuzzy #~ msgid "Allow to dismiss screen overlay by gesture when a video is playing" #~ msgstr "允许在播放视频时通过手势关闭屏幕覆盖层" #, fuzzy #~ msgid "Added Georgian translation (thanks @NorwayFun)" #~ msgstr "添加了格鲁吉亚语翻译(感谢 @NorwayFun)" #, fuzzy #~ msgid "Adjusted translations in appdata (thanks @yakushabb)" #~ msgstr "调整了 appdata 中的翻译(感谢 @yakushabb)" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.2" #~ msgstr "gnome-pomodoro 0.25.2 更新概览" #, fuzzy #~ msgid "Fix keeping notification after extending Pomodoro" #~ msgstr "修复了延长番茄钟后通知残留的问题" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.1" #~ msgstr "gnome-pomodoro 0.25.1 更新概览" #, fuzzy #~ msgid "Fixes for GNOME Shell 46" #~ msgstr "修复了 GNOME Shell 46 的问题" #, fuzzy #~ msgid "Drop support for GNOME Shell 45" #~ msgstr "停止支持 GNOME Shell 45" #, fuzzy #~ msgid "Overview of changes in gnome-pomodoro 0.25.0" #~ msgstr "gnome-pomodoro 0.25.0 更新概览" #, fuzzy #~ msgid "Support for GNOME Shell 46" #~ msgstr "支持 GNOME Shell 46" #, fuzzy #~ msgid "Adjust build script to meson 0.59.0 (thanks @mattst88)" #~ msgstr "调整构建脚本以支持 meson 0.59.0(感谢 @mattst88)" #, fuzzy #~ msgid "" #~ "Let Pomodoro manage system notifications while the timer is running" #~ msgstr "在计时器运行时,让 番茄计时器 管理系统通知" #, fuzzy #~ msgid "15 seconds" #~ msgstr "15 秒" #, fuzzy #~ msgid "30 seconds" #~ msgstr "30 秒" #, fuzzy #~ msgid "1 minute" #~ msgstr "1 分钟" #, fuzzy #~ msgid "2 minutes" #~ msgstr "2 分钟" #, fuzzy #~ msgid "3 minutes" #~ msgstr "3 分钟" #, fuzzy #~ msgid "5 minutes" #~ msgstr "5 分钟" #~ msgid "Timer Ticking" #~ msgstr "秒表滴答" #, fuzzy #~ msgid "Birds" #~ msgstr "鸟鸣" #~ msgid "@APPLICATION_NAME@" #~ msgstr "@APPLICATION_NAME@" #~ msgid "timer;" #~ msgstr "计时器;" #~ msgid "Start/Stop" #~ msgstr "开始/停止" #~ msgid "Pause/Resume" #~ msgstr "暂停/继续" #~ msgid "Skip to a pomodoro or to a break" #~ msgstr "跳到一个番茄钟或休息一下" #~ msgid "Reset current session" #~ msgstr "重置当前会话" #~ msgid "Run as background service" #~ msgstr "作为后台服务运行" #~ msgid "About Pomodoro" #~ msgstr "关于番茄计时器" #~ msgid "A simple time management utility" #~ msgstr "一个简易的时间管理工具" #, fuzzy #~ msgid "_Stopped" #~ msgstr "停止" #, fuzzy #~ msgid "Extension for GNOME Shell is available" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Failed to install extension" #~ msgstr "未能开启扩展" #, fuzzy, c-format #~ msgid "Long break due in %s" #~ msgstr "长休息时长" #~ msgid "A time management utility for GNOME" #~ msgstr "一个GNOME下的时间管理工具" #~ msgid "" #~ "A GNOME utility that helps managing time according to Pomodoro Technique. " #~ "It intends to improve productivity and focus by taking short breaks after " #~ "every 25 minutes of work." #~ msgstr "" #~ "一个基于番茄时间管理法的GNOME应用,利用每工作25分钟后一个短暂停的方法,提" #~ "高工作效率以及专注度。" #~ msgid "Timer window" #~ msgstr "计时器窗口" #~ msgid "Indicator for GNOME Shell" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 42 (@milotype and @kappa)" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 41 (@mbooth101)" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support GNOME Shell 40.0, not 4.0" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 4.0" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 3.38 (@ignapk and @szpak)" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 3.36" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 3.34 only" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 3.32 (@demokritos)" #~ msgstr "GNOME Shell下的程序指示器" #, fuzzy #~ msgid "Support for GNOME Shell 3.28 and 3.30 (@aerostitch)" #~ msgstr "GNOME Shell下的程序指示器" #~ msgid "_Timer" #~ msgstr "计时器(_T)" #~ msgid "Keyboard shortcut to toggle the timer. Enter new shortcut to change." #~ msgstr "用于计时器开关的键盘快捷键。输入新的快捷键进行改变。" #~ msgid "Pomodoros before a long break" #~ msgstr "进入长休息前的番茄钟数目" #~ msgid "Keyboard shortcut" #~ msgstr "键盘快捷键" #~ msgid "Screen notifications" #~ msgstr "全屏通知" #~ msgid "Wait for activity after a break" #~ msgstr "休息后等待" #~ msgid "Plugins…" #~ msgstr "插件……" #~ msgid "Plugins" #~ msgstr "插件" #~ msgid "Back" #~ msgstr "返回" #~ msgid "Complete a few sessions" #~ msgstr "请先完成一些番茄钟" #~ msgid "Previous (Alt+Left)" #~ msgstr "前一个(Alt+Left)" #~ msgid "Next (Alt+Right)" #~ msgstr "后一个(Alt+Right)" #~ msgid "Complete" #~ msgstr "完成" #~ msgid "Enable" #~ msgstr "启用" #~ msgid "Add" #~ msgstr "添加" #~ msgid "Remove" #~ msgstr "移除" #~ msgid "Elapsed Time" #~ msgstr "流逝时间" #~ msgid "Pause Timer" #~ msgstr "暂停计时" #~ msgid "Pause break" #~ msgstr "暂停" #~ msgid "Pause Pomodoro" #~ msgstr "暂停" #~ msgid "Resume break" #~ msgstr "继续" #~ msgid "Resume Pomodoro" #~ msgstr "继续" #, javascript-format #~ msgid "%d minute remaining" #~ msgid_plural "%d minutes remaining" #~ msgstr[0] "剩余 %d 分钟" #~ msgid "Report issue" #~ msgstr "报告问题" #, javascript-format #~ msgid "Failed to run %s service" #~ msgstr "未能运行 %s 服务" #~ msgid "Woodland Birds" #~ msgstr "林地鸟语" #~ msgid "End of Break Sound" #~ msgstr "结束休息的声音" #~ msgid "Start of Break Sound" #~ msgstr "开始休息的声音" #~ msgid "Off" #~ msgstr "关闭" #~ msgid "Ticking sound" #~ msgstr "滴答声" #~ msgid "Start of break sound" #~ msgstr "开始休息的声音" #~ msgid "End of break sound" #~ msgstr "结束休息的声音" #~ msgid "Focus on your task." #~ msgstr "请专注于您的任务。" #, c-format #~ msgid "You have %d minute" #~ msgid_plural "You have %d minutes" #~ msgstr[0] "您有%d分钟" #, c-format #~ msgid "You have %d second" #~ msgid_plural "You have %d seconds" #~ msgstr[0] "您有%d秒" #~ msgid "Take a longer break" #~ msgstr "休息久一点吧" #~ msgid "Lengthen it" #~ msgstr "延长" #~ msgid "Shorten it" #~ msgstr "缩短" #~ msgid "Start pomodoro" #~ msgstr "开始新番茄钟" #, c-format #~ msgid "" #~ "Using \"%s\" as shortcut will interfere with typing. Try adding another " #~ "key, such as Control, Alt or Shift." #~ msgstr "" #~ "使用 %s 作为快捷键会影响正常键盘输入。请尝试添加修饰键,如Ctrl,Alt或" #~ "Shift 。" #~ msgid "Available" #~ msgstr "空闲" #~ msgid "Busy" #~ msgstr "忙碌" #~ msgid "Idle" #~ msgstr "发呆" #~ msgid "Invisible" #~ msgstr "不可见" #, c-format #~ msgid "%d m" #~ msgstr "%d 分钟" #, c-format #~ msgid "%.0f h" #~ msgstr "%.0f 小时" #, c-format #~ msgid "%.1f h" #~ msgstr "%.1f 小时" #~ msgid "gnome-pomodoro" #~ msgstr "gnome-pomodoro" #~ msgid "_Stats" #~ msgstr "统计(_S)" #~ msgid "It seems to be uninstalled" #~ msgstr "似乎已卸载" #~ msgid "Extension is out of date" #~ msgstr "扩展已过期" #~ msgid "Upgrade" #~ msgstr "升级" focustimerhq-FocusTimer-8581be2/run-vala-lint.sh000077500000000000000000000003261520625676500217100ustar00rootroot00000000000000#!/bin/sh PROJECT_PATH="`dirname "$0"`" CONFIG_PATH="lint/vala-lint.ini" FILES=( "src/*.vala" "tests/*.vala" "plugins/*/*.vala" ) cd $PROJECT_PATH io.elementary.vala-lint --config=$CONFIG_PATH $FILES focustimerhq-FocusTimer-8581be2/src/000077500000000000000000000000001520625676500174465ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/application.vala000066400000000000000000001210351520625676500226200ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public enum ExitStatus { UNDEFINED = -1, SUCCESS = 0, FAILURE = 1 } public class Application : Adw.Application { [Compact] private class Option { public string long_name; public char short_name; public string description; public GLib.OptionArg arg_type; public string? arg_description; public string? group; public string? action_name; public GLib.Variant? action_parameter; public bool is_exclusive; public bool bool_value; public int int_value; public Option (string long_name, char short_name, string description, GLib.OptionArg arg_type, string? arg_description, string? action_name = null, GLib.Variant? action_parameter = null, bool is_exclusive = true) { var parts = long_name.split (".", 2); if (parts.length > 1) { this.group = parts[0]; this.long_name = parts[1]; } else { this.group = "main"; this.long_name = long_name; } this.short_name = short_name; this.description = description; this.arg_description = arg_description; this.arg_type = arg_type; this.action_name = action_name; this.action_parameter = action_parameter; this.is_exclusive = is_exclusive; } public void* get_arg_data () { switch (this.arg_type) { case GLib.OptionArg.NONE: return (void*) (&this.bool_value); case GLib.OptionArg.INT: return (void*) (&this.int_value); default: assert_not_reached (); } } public bool is_set () { switch (this.arg_type) { case GLib.OptionArg.NONE: return this.bool_value; case GLib.OptionArg.INT: return this.int_value > 0; default: assert_not_reached (); } } public GLib.Variant? get_value () { switch (this.arg_type) { case GLib.OptionArg.NONE: return new GLib.Variant.boolean (true); case GLib.OptionArg.INT: return new GLib.Variant.int32 (this.int_value); default: assert_not_reached (); } } public GLib.Variant? get_action_parameter (GLib.Variant? value) { return this.arg_type != GLib.OptionArg.NONE ? value : this.action_parameter; } } private static Option[] OPTIONS; public Ft.Timer? timer; public Ft.SessionManager? session_manager; private Ft.KeyboardManager? keyboard_manager; private Ft.StatsManager? stats_manager; private Ft.EventProducer? event_producer; private Ft.EventBus? event_bus; private Ft.JobQueue? job_queue; private Ft.ActionManager? action_manager; private Ft.NotificationManager? notification_manager; private Ft.ScreenOverlayManager? screen_overlay_manager; private Ft.BackgroundManager? background_manager; private Ft.SoundManager? sound_manager; private Ft.ScreenSaver? screensaver; private Ft.Indicator? indicator; private Ft.Logger? logger; private GLib.Settings? settings; private Peas.ExtensionSet? extensions; private uint save_idle_id = 0; private Ft.ApplicationDBusService? dbus_service; private uint dbus_service_id; private Ft.TimerDBusService? timer_dbus_service; private uint timer_dbus_service_id; private Ft.SessionDBusService? session_dbus_service; private uint session_dbus_service_id; private uint service_hold_id = 0U; private bool preferences_requested = false; static construct { OPTIONS = { new Option ("timer.start-stop", '\0', N_("Start or Stop"), GLib.OptionArg.NONE, null, "timer.start-stop"), new Option ("timer.start-pause-resume", '\0', N_("Start, Pause or Resume"), GLib.OptionArg.NONE, null, "timer.start-pause-resume"), new Option ("timer.start-pomodoro", '\0', N_("Start Pomodoro"), GLib.OptionArg.NONE, null, "session-manager.state", new GLib.Variant.string ("pomodoro")), new Option ("timer.start-break", '\0', N_("Start break"), GLib.OptionArg.NONE, null, "session-manager.state", new GLib.Variant.string ("break")), new Option ("timer.start-short-break", '\0', N_("Start short break"), GLib.OptionArg.NONE, null, "session-manager.state", new GLib.Variant.string ("short-break")), new Option ("timer.start-long-break", '\0', N_("Start long break"), GLib.OptionArg.NONE, null, "session-manager.state", new GLib.Variant.string ("long-break")), new Option ("timer.start", '\0', N_("Start"), GLib.OptionArg.NONE, null, "timer.start"), new Option ("timer.stop", '\0', N_("Stop"), GLib.OptionArg.NONE, null, "timer.reset"), new Option ("timer.pause", '\0', N_("Pause"), GLib.OptionArg.NONE, null, "timer.pause"), new Option ("timer.resume", '\0', N_("Resume"), GLib.OptionArg.NONE, null, "timer.resume"), new Option ("timer.skip", '\0', N_("Skip"), GLib.OptionArg.NONE, null, "session-manager.advance"), new Option ("timer.rewind", '\0', N_("Rewind"), GLib.OptionArg.INT, N_("SECONDS"), "timer.rewind-by"), new Option ("timer.extend", '\0', N_("Extend current pomodoro or break"), GLib.OptionArg.INT, N_("SECONDS"), "timer.extend-by"), new Option ("timer.reset", '\0', N_("Reset"), GLib.OptionArg.NONE, null, "session-manager.reset"), new Option ("timer.status", 's', N_("Print timer status"), GLib.OptionArg.NONE, null, null, null, false), new Option ("preferences", '\0', N_("Show preferences"), GLib.OptionArg.NONE, null, null, null, false), new Option ("quit", 'q', N_("Quit application"), GLib.OptionArg.NONE, null, "app.quit"), new Option ("version", 'v', N_("Print version information and exit"), GLib.OptionArg.NONE, null) }; } private static GLib.OptionEntry[] get_option_entries (string group) { GLib.OptionEntry[] result = {}; foreach (unowned var option in OPTIONS) { if (option.group == group) { result += GLib.OptionEntry () { long_name = option.long_name, short_name = option.short_name, description = option.description, flags = GLib.OptionFlags.NONE, arg = option.arg_type, arg_data = option.get_arg_data (), arg_description = option.arg_description }; } } result += GLib.OptionEntry (); // null entry return result; } construct { var timer_options = new GLib.OptionGroup ( "timer", _("Timer Options:"), _("Show options for controlling the timer")); timer_options.add_entries (get_option_entries ("timer")); timer_options.set_translation_domain (Config.GETTEXT_PACKAGE); this.add_option_group (timer_options); this.add_main_option_entries (get_option_entries ("main")); this.set_option_context_description ( _("Bugs may be reported at: %s").printf (Config.PACKAGE_ISSUE_URL)); } public Application () { GLib.Object ( application_id: Config.APPLICATION_ID, flags: GLib.ApplicationFlags.HANDLES_COMMAND_LINE, resource_base_path: "/io/github/focustimerhq/FocusTimer/" ); } public new static unowned Ft.Application get_default () { return GLib.Application.get_default () as Ft.Application; } public unowned Gtk.Window? get_last_focused_window () { unowned GLib.List link = this.get_windows (); return link != null ? link.first ().data : null; } public unowned T? get_window () { unowned GLib.List link = this.get_windows (); var window_type = typeof (T); while (link != null) { if (link.data.get_type () == window_type) { return link.data; } link = link.next; } return null; } private void schedule_save () { if (this.save_idle_id != 0) { return; } this.save_idle_id = GLib.Idle.add (() => { this.save_idle_id = 0; this.session_manager.save.begin ((obj, res) => { this.session_manager.save.end (res); }); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.save_idle_id, "Ft.Application.schedule_save"); } public void show_window (Ft.WindowView view = Ft.WindowView.DEFAULT) { var window = this.get_window (); var window_created = false; if (window == null) { window = new Ft.Window (); window_created = true; this.add_window (window); if (Ft.is_devel ()) { window.add_css_class ("devel"); } } if (view != Ft.WindowView.DEFAULT) { window.size = Ft.WindowSize.NORMAL; window.view = view; } window.present (); if (!window.is_active && !window_created) { this.dbus_service.emit_request_focus (); } } public void show_preferences (string panel_name = "") { var preferences_window = this.get_window (); if (preferences_window == null) { preferences_window = new Ft.PreferencesWindow (); this.add_window (preferences_window); } if (panel_name != "") { preferences_window.select_panel (panel_name); } preferences_window.present (); } private void show_about_dialog () { var window = this.get_window (); var about_dialog = this.get_window (); if (about_dialog == null) { about_dialog = Ft.create_about_dialog (); } about_dialog.present (window); } private void show_screen_overlay () { if (!this.session_manager.current_state.is_break () || this.timer.is_finished ()) { this.session_manager.advance_to_state (Ft.State.BREAK); } else if (this.timer.is_paused ()) { this.timer.resume (); } else if (!this.timer.is_started ()) { this.timer.start (); } this.screen_overlay_manager.open (); } private void activate_prefixed_action (string action_name, GLib.Variant? parameter) { GLib.ActionGroup action_group; var parts = action_name.split (".", 2); if (parts.length < 2) { this.activate_action (action_name, parameter); return; } switch (parts[0]) { case "app": action_group = (GLib.ActionGroup) this; break; case "timer": action_group = new Ft.TimerActionGroup (); break; case "session-manager": action_group = new Ft.SessionManagerActionGroup (); break; default: GLib.warning ("Unhandled action '%s'", action_name); return; } action_group.activate_action (parts[1], parameter); } private void activate_window (GLib.SimpleAction action, GLib.Variant? parameter) { var view = Ft.WindowView.from_string (parameter.get_string ()); this.show_window (view); } private void activate_toggle_window (GLib.SimpleAction action, GLib.Variant? parameter) { var window = this.get_window (); if (window == null || !window.is_active) { this.show_window (Ft.WindowView.TIMER); } else { window.close_to_background (); } } private void activate_preferences (GLib.SimpleAction action, GLib.Variant? parameter) { this.show_preferences (); } private void activate_log (GLib.SimpleAction action, GLib.Variant? parameter) { var log_window = this.get_window (); if (log_window == null) { log_window = new Ft.LogWindow (); this.add_window (log_window); } if (parameter != null) { log_window.select ((ulong) parameter.get_uint64 ()); } log_window.present (); } private void activate_about (GLib.SimpleAction action, GLib.Variant? parameter) { this.show_about_dialog (); } private void activate_screen_overlay (GLib.SimpleAction action, GLib.Variant? parameter) { this.show_screen_overlay (); } private void activate_visit_website (GLib.SimpleAction action, GLib.Variant? parameter) { try { string[] spawn_args = { "xdg-open", Config.PACKAGE_WEBSITE }; string[] spawn_env = GLib.Environ.get (); GLib.Process.spawn_async (null, spawn_args, spawn_env, GLib.SpawnFlags.SEARCH_PATH, null, null); } catch (GLib.SpawnError error) { GLib.warning ("Failed to spawn process: %s", error.message); } } private void activate_report_issue (GLib.SimpleAction action, GLib.Variant? parameter) { try { string[] spawn_args = { "xdg-open", Config.PACKAGE_ISSUE_URL }; string[] spawn_env = GLib.Environ.get (); GLib.Process.spawn_async (null, spawn_args, spawn_env, GLib.SpawnFlags.SEARCH_PATH, null, null); } catch (GLib.SpawnError error) { GLib.warning ("Failed to spawn process: %s", error.message); } } private void activate_quit (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("application.quit"); this.quit (); } private void activate_advance (GLib.SimpleAction action, GLib.Variant? parameter) { this.activate_prefixed_action ("session-manager.advance", parameter); } private void activate_advance_to_state (GLib.SimpleAction action, GLib.Variant? parameter) { this.activate_prefixed_action ("session-manager.state", parameter); } private void activate_extend (GLib.SimpleAction action, GLib.Variant? parameter) { this.activate_prefixed_action ("timer.extend-by", parameter); } private void setup_resources () { var display = Gdk.Display.get_default (); var icon_theme = Gtk.IconTheme.get_for_display (display); icon_theme.add_resource_path ("/io/github/focustimerhq/FocusTimer/icons"); } private void setup_database () { Ft.Database.open (); } private void setup_actions () { GLib.SimpleAction action; action = new GLib.SimpleAction ("window", GLib.VariantType.STRING); action.activate.connect (this.activate_window); this.add_action (action); action = new GLib.SimpleAction ("toggle-window", null); action.activate.connect (this.activate_toggle_window); this.add_action (action); action = new GLib.SimpleAction ("preferences", null); action.activate.connect (this.activate_preferences); this.add_action (action); action = new GLib.SimpleAction ("log", GLib.VariantType.UINT64); action.activate.connect (this.activate_log); this.add_action (action); action = new GLib.SimpleAction ("screen-overlay", null); action.activate.connect (this.activate_screen_overlay); this.add_action (action); action = new GLib.SimpleAction ("visit-website", null); action.activate.connect (this.activate_visit_website); this.add_action (action); action = new GLib.SimpleAction ("report-issue", null); action.activate.connect (this.activate_report_issue); this.add_action (action); action = new GLib.SimpleAction ("about", null); action.activate.connect (this.activate_about); this.add_action (action); action = new GLib.SimpleAction ("quit", null); action.activate.connect (this.activate_quit); this.add_action (action); // Include timer and session-manager actions under the "app" namespace // for use in notifications. action = new GLib.SimpleAction ("advance", null); action.activate.connect (this.activate_advance); this.add_action (action); action = new GLib.SimpleAction ("advance-to-state", GLib.VariantType.STRING); action.activate.connect (this.activate_advance_to_state); this.add_action (action); action = new GLib.SimpleAction ("extend", GLib.VariantType.INT32); action.activate.connect (this.activate_extend); this.add_action (action); this.set_accels_for_action ("app.preferences", {"comma"}); this.set_accels_for_action ("app.log", {"l"}); this.set_accels_for_action ("app.quit", {"q"}); this.set_accels_for_action ("window.close", {"w"}); this.set_accels_for_action ("win.toggle-compact-size", {"F9"}); this.keyboard_manager = new Ft.KeyboardManager (); this.keyboard_manager.add_shortcut ("timer.start-stop", _("Start or Stop"), "p"); this.keyboard_manager.add_shortcut ("timer.start-pause-resume", _("Start, Pause or Resume")); this.keyboard_manager.add_shortcut ("timer.start", _("Start")); this.keyboard_manager.add_shortcut ("timer.reset", _("Stop")); this.keyboard_manager.add_shortcut ("timer.pause", _("Pause")); this.keyboard_manager.add_shortcut ("timer.resume", _("Resume")); this.keyboard_manager.add_shortcut ("session-manager.advance", _("Skip")); this.keyboard_manager.add_shortcut ("timer.rewind", _("Rewind")); this.keyboard_manager.add_shortcut ("app.toggle-window", _("Bring to Focus"), "p"); this.keyboard_manager.shortcut_activated.connect (this.on_shortcut_activated); } private void setup_providers () { this.screen_overlay_manager.add_provider ( new Ft.DefaultScreenOverlayProvider (), Ft.Priority.DEFAULT); this.screensaver.add_provider ( new Ft.DefaultScreenSaverProvider (), Ft.Priority.DEFAULT); } private void setup_plugins () { var engine = Peas.Engine.get_default (); engine.add_search_path ("resource:///plugins", "resource:///plugins"); engine.rescan_plugins (); var n_plugins = engine.get_n_items (); for (var i = 0U; i < n_plugins; i++) { var plugin_info = (Peas.PluginInfo) engine.get_item (i); engine.load_plugin (plugin_info); } this.extensions = new Peas.ExtensionSet.with_properties ( engine, typeof (Ft.ApplicationExtension), {}, {}); } private void update_color_scheme () { var style_manager = Adw.StyleManager.get_default (); if (this.settings.get_boolean ("dark-theme")) { style_manager.set_color_scheme (Adw.ColorScheme.FORCE_DARK); } else { style_manager.set_color_scheme (Adw.ColorScheme.DEFAULT); } } /** * Emitted on the primary instance immediately after registration. */ public override void startup () { var dbus_connection = this.get_dbus_connection (); var dbus_object_path = this.get_dbus_object_path (); var main_context = GLib.MainContext.@default (); var ready = false; this.hold (); this.mark_busy (); base.startup (); this.settings = Ft.get_settings (); this.session_manager = Ft.SessionManager.get_default (); this.timer = this.session_manager.timer; this.stats_manager = new Ft.StatsManager (); this.notification_manager = new Ft.NotificationManager (); this.screen_overlay_manager = new Ft.ScreenOverlayManager (); this.background_manager = new Ft.BackgroundManager (); this.event_producer = new Ft.EventProducer (); this.event_bus = this.event_producer.bus; this.job_queue = new Ft.JobQueue (); this.logger = new Ft.Logger (); this.indicator = new Ft.Indicator (); this.screensaver = new Ft.ScreenSaver (); #if ENABLE_AUTOMATION this.action_manager = new Ft.ActionManager (); #endif this.sound_manager = this.settings.get_boolean ("sounds") ? new Ft.SoundManager () : null; this.setup_resources (); this.setup_database (); this.setup_plugins (); this.setup_providers (); this.setup_actions (); this.update_color_scheme (); this.settings.changed.connect (this.on_settings_changed); this.event_bus.event.connect (this.on_event); this.session_manager.restore.begin ( Ft.Timestamp.UNDEFINED, (obj, res) => { this.session_manager.restore.end (res); this.session_manager.ensure_session (); this.session_manager.enter_session.connect (this.on_enter_session); this.session_manager.leave_session.connect (this.on_leave_session); this.session_manager.advanced.connect (this.on_advanced); this.on_enter_session (this.session_manager.current_session); ready = true; main_context.wakeup (); }); if (this.timer_dbus_service == null) { try { this.timer_dbus_service = new Ft.TimerDBusService ( dbus_connection, dbus_object_path, this.timer, this.session_manager); this.timer_dbus_service_id = dbus_connection.register_object ( dbus_object_path, this.timer_dbus_service); } catch (GLib.IOError error) { GLib.warning ("Error while initializing timer dbus service: %s", error.message); this.timer_dbus_service = null; } } if (this.session_dbus_service == null) { try { this.session_dbus_service = new Ft.SessionDBusService ( dbus_connection, dbus_object_path, this.session_manager); this.session_dbus_service_id = dbus_connection.register_object ( dbus_object_path, this.session_dbus_service); } catch (GLib.IOError error) { GLib.warning ("Error while initializing session dbus service: %s", error.message); this.session_dbus_service = null; } } if (GLib.ApplicationFlags.IS_SERVICE in this.flags) { this.background_manager.hold.begin ( "", (obj, res) => { this.service_hold_id = this.background_manager.hold.end (res); }); } while (!ready) { main_context.iteration (true); } this.unmark_busy (); this.release (); } private void print_timer_status (GLib.ApplicationCommandLine command_line) { var timestamp = this.timer.get_current_time (); var last_state_changed_time = this.timer.get_last_state_changed_time (); if (timestamp - last_state_changed_time < Ft.Interval.SECOND) { timestamp = last_state_changed_time; } var message = new GLib.StringBuilder (); var glyph = " "; if (this.timer.is_running ()) { glyph = "▶"; } else if (this.timer.is_paused ()) { glyph = "⏸"; } else if (!this.timer.is_started ()) { glyph = "⏹"; } message.append_printf (" %s %s\n", glyph, this.session_manager.current_state.get_label ()); if (this.session_manager.current_state != Ft.State.STOPPED) { var seconds_uint = (uint) Ft.Timestamp.to_seconds_uint ( this.timer.calculate_remaining (timestamp)); message.append_printf ( " %s\n", _("%s remaining").printf (Ft.format_time (seconds_uint))); } command_line.print_literal (message.str); } private void print_version () { stdout.printf ("%s %s\n", GLib.Environment.get_application_name (), Config.PACKAGE_VERSION); } /** * GLib only fills `options` for entries with empty `arg_data`, and empty `arg_data` aren't * supported for option groups. Therefore, we fill `options` manually before it's passed * to the remote instance. */ public override int handle_local_options (GLib.VariantDict options) { var exclusive_options_count = 0U; var version_requested = false; foreach (unowned var option in OPTIONS) { if (option.is_set ()) { if (option.is_exclusive) { exclusive_options_count++; } if (option.long_name == "version") { version_requested = true; continue; } options.insert_value (option.long_name, option.get_value ()); } } if (exclusive_options_count > 1U) { stderr.printf ( "%s\n", _("Invalid use. Pass one flag for controlling the timer at a time.")); return ExitStatus.FAILURE; } if (version_requested) { this.print_version (); return Ft.ExitStatus.SUCCESS; } return Ft.ExitStatus.UNDEFINED; } public override int command_line (GLib.ApplicationCommandLine command_line) { var options = command_line.get_options_dict (); var exit_status = ExitStatus.UNDEFINED; foreach (unowned var option in OPTIONS) { var value = options.lookup_value (option.long_name, null); if (value == null) { continue; } if (option.action_name != null) { this.activate_prefixed_action (option.action_name, option.get_action_parameter (value)); exit_status = Ft.ExitStatus.SUCCESS; } else if (option.long_name == "status") { this.print_timer_status (command_line); exit_status = Ft.ExitStatus.SUCCESS; } } if (exit_status != ExitStatus.UNDEFINED) { return exit_status; } this.preferences_requested = options.contains ("preferences"); this.activate (); this.preferences_requested = false; return Ft.ExitStatus.SUCCESS; } public override void activate () { if (this.preferences_requested) { this.show_preferences (); } else { this.show_window (); } } /* Save the state before exit. * * Emitted only on the registered primary instance immediately after * the main loop terminates. */ public override void shutdown () { if (this.service_hold_id != 0U) { this.background_manager.release (this.service_hold_id); this.service_hold_id = 0U; } if (this.save_idle_id != 0) { GLib.Source.remove (this.save_idle_id); this.save_idle_id = 0; } this.settings.changed.disconnect (this.on_settings_changed); this.event_bus.event.disconnect (this.on_event); this.keyboard_manager.shortcut_activated.disconnect (this.on_shortcut_activated); base.shutdown (); // Stop emitting new events this.event_producer.destroy (); this.event_bus.destroy (); this.action_manager?.destroy (); this.background_manager.destroy (); this.session_manager.enter_session.disconnect (this.on_enter_session); this.session_manager.leave_session.disconnect (this.on_leave_session); this.session_manager.advanced.disconnect (this.on_advanced); if (this.session_manager.current_session != null) { this.session_manager.current_session.changed.disconnect ( this.on_current_session_changed); } // Pause the timer before saving the session this.timer.pause (); // Wait until all async jobs are completed var main_context = GLib.MainContext.@default (); var remaining = 2; this.job_queue.wait.begin ( (obj, res) => { this.job_queue.wait.end (res); remaining--; }); this.session_manager.save.begin ( (obj, res) => { this.session_manager.save.end (res); remaining--; }); while (remaining > 0 && main_context.iteration (true)); // Cleanup Ft.Database.close (); Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); this.event_producer = null; this.event_bus = null; this.extensions = null; this.job_queue = null; this.action_manager = null; this.logger = null; this.indicator = null; this.screensaver = null; this.background_manager = null; this.sound_manager = null; this.notification_manager = null; this.screen_overlay_manager = null; this.keyboard_manager = null; this.stats_manager = null; this.session_manager = null; this.timer = null; this.settings = null; } /** * Register main D-Bus service for the app. */ public override bool dbus_register (GLib.DBusConnection connection, string object_path) throws GLib.Error { try { base.dbus_register (connection, object_path); if (this.dbus_service == null) { this.dbus_service = new Ft.ApplicationDBusService ( connection, object_path, this, Ft.get_settings ()); this.dbus_service_id = connection.register_object (object_path, dbus_service); } } catch (GLib.IOError error) { GLib.warning ("Error while initializing dbus service: %s", error.message); this.dbus_service = null; throw error; } return true; } public override void dbus_unregister (GLib.DBusConnection connection, string object_path) { if (this.timer_dbus_service != null) { connection.unregister_object (this.timer_dbus_service_id); this.timer_dbus_service = null; this.timer_dbus_service_id = 0U; } if (this.session_dbus_service != null) { connection.unregister_object (this.session_dbus_service_id); this.session_dbus_service = null; this.session_dbus_service_id = 0U; } if (this.dbus_service != null) { connection.unregister_object (this.dbus_service_id); this.dbus_service = null; this.dbus_service_id = 0U; } base.dbus_unregister (connection, object_path); } public new void send_notification (string id, Ft.Notification notification) { this.notification_manager?.backend.send_notification (id, notification); } public new void withdraw_notification (string id) { this.notification_manager?.backend.withdraw_notification (id); } private void on_settings_changed (GLib.Settings settings, string key) { switch (key) { case "dark-theme": this.update_color_scheme (); break; case "sounds": this.sound_manager = settings.get_boolean (key) ? new Ft.SoundManager () : null; break; } } private void on_current_session_changed () { this.schedule_save (); } private void on_enter_session (Ft.Session session) { session.changed.connect (this.on_current_session_changed); } private void on_leave_session (Ft.Session session) { session.changed.disconnect (this.on_current_session_changed); Ft.Database.schedule_backup (); } private void on_advanced (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block) { this.schedule_save (); } private void on_event (Ft.Event event) { this.logger.log_event (event); } private void on_shortcut_activated (string shortcut_name) { this.activate_prefixed_action (shortcut_name, null); } public override void dispose () { assert (this.save_idle_id == 0); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/000077500000000000000000000000001520625676500203765ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/core/action-list-model.vala000066400000000000000000000245231520625676500245750ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * A list model for storing actions in settings. * * Each action is stored using an relocatable schema. There is no API to list relocatable * schemas or their subfolders, so we need to keep subfolders as `actions` key. `actions` * also stores the actions order. */ public class ActionListModel : GLib.Object, GLib.ListModel { public uint n_items { get { return this._n_items; } } private GLib.HashTable actions; private string[] uuids; private uint _n_items = 0; private GLib.Settings settings = null; private ulong settings_changed_id = 0; private int settings_changed_inhibit_count = 0; construct { this.uuids = new string[0]; this.actions = new GLib.HashTable (GLib.str_hash, GLib.str_equal); this.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer.actions"); this.settings_changed_id = this.settings.changed.connect (this.on_settings_changed); this.load (); } private void load_uuids () { var uuids = this.settings.get_strv ("actions"); var unique_uuids = new GLib.GenericSet (GLib.str_hash, GLib.str_equal); this.uuids.resize (0); this._n_items = 0; foreach (var uuid in uuids) { if (!unique_uuids.contains (uuid)) { this.uuids += uuid; this._n_items++; unique_uuids.add (uuid); } } } private void save_uuids () { this.settings.set_strv ("actions", this.uuids); } private GLib.Settings create_action_settings (string uuid) requires (uuid != null && uuid != "") { var existing_settings = this.actions.lookup (uuid)?.settings; if (existing_settings != null) { return existing_settings; } return new GLib.Settings.with_path ("io.github.focustimerhq.FocusTimer.actions.action", @"/io/github/focustimerhq/FocusTimer/actions/$(uuid)/"); } private void inhibit_settings_changed () { this.settings_changed_inhibit_count++; } private void uninhibit_settings_changed () { this.settings_changed_inhibit_count--; } private Ft.Action load_action (string uuid) { var settings = this.create_action_settings (uuid); var action = this.create_action (uuid, settings.get_enum ("trigger")); action.load (settings); this.actions.insert (uuid, action); settings.changed.connect ( (key) => { if (this.settings_changed_inhibit_count > 0) { return; } if (key == "trigger") { this.reload_action (uuid); } }); return action; } private void reload_action (string uuid) { var action = this.load_action (uuid); var position = this.index (action.uuid); if (position >= 0) { this.items_changed ((uint) position, 1U, 1U); } } private void load () { var previous_n_items = this._n_items; this.load_uuids (); foreach (var uuid in this.uuids) { this.load_action (uuid); } this.items_changed (0, this._n_items, previous_n_items); } private void on_settings_changed (GLib.Settings settings, string key) { if (this.settings_changed_inhibit_count > 0) { return; } switch (key) { case "actions": // TODO: detect which actions were removed and update `this.uuids` break; } } public GLib.Type get_item_type () { return typeof (Ft.Action); } public uint get_n_items () { return this._n_items; } public GLib.Object? get_item (uint position) { if (position >= this._n_items) { return null; } var uuid = this.uuids[position]; var action = this.actions.lookup (uuid); if (action == null) { action = this.load_action (uuid); } return (GLib.Object?) action; } public int index (string uuid) { for (var position = 0; position < this._n_items; position++) { if (this.uuids[position] == uuid) { return position; } } return -1; } public Ft.Action? lookup (string uuid) { return this.actions.lookup (uuid); } /** * Initialize an action instance. */ public Ft.Action create_action (string? uuid = null, Ft.ActionTrigger trigger = Ft.ActionTrigger.EVENT) { switch (trigger) { case Ft.ActionTrigger.EVENT: return new Ft.EventAction (uuid); case Ft.ActionTrigger.CONDITION: return new Ft.ConditionAction (uuid); default: assert_not_reached (); } } public void save_action (Ft.Action action) { var creating = action.uuid == null; var settings = action.settings; var position = 0U; Ft.Action? previous_action = null; if (creating) { action.set_uuid_internal (GLib.Uuid.string_random ()); position = this._n_items; } else { var index = this.index (action.uuid); if (index >= 0) { position = (uint) index; previous_action = this.actions.lookup (action.uuid); } else { creating = true; position = this._n_items; } } if (settings == null) { settings = this.create_action_settings (action.uuid); } this.actions.insert (action.uuid, action); this.inhibit_settings_changed (); action.save (settings); this.uninhibit_settings_changed (); if (creating) { this.uuids += action.uuid; this._n_items++; this.save_uuids (); this.action_added (action); this.items_changed (position, 0U, 1U); } else { this.action_removed (previous_action); this.action_added (action); this.items_changed (position, 1U, 1U); } } public void delete_action (string uuid) { var action = this.actions.lookup (uuid); if (action == null) { return; } var new_uuids = new string[0]; var changed = false; var position = 0U; var settings = action.settings; foreach (var _uuid in this.uuids) { if (_uuid != uuid) { new_uuids += _uuid; if (!changed) { position++; } } else { changed = true; } } if (settings != null) { this.inhibit_settings_changed (); foreach (var key in settings.settings_schema.list_keys ()) { settings.reset (key); } this.uninhibit_settings_changed (); } this.actions.remove (uuid); if (changed) { this.uuids = new_uuids; this._n_items = new_uuids.length; this.save_uuids (); this.action_removed (action); this.items_changed (position, 1U, 0U); } } public void move_action (string uuid, uint position) { var source_position = this.index (uuid); var destination_position = int.min ((int) position, this.uuids.length - 1); var n_changes = (source_position - destination_position).abs (); var direction = destination_position > source_position ? 1 // move source id to the right : -1; // move source id to the left if (source_position < 0 || destination_position < 0 || n_changes == 0) { return; } for (var i = 0; i < n_changes; i++) { this.uuids[source_position + i * direction] = this.uuids[source_position + (i + 1) * direction]; } this.uuids[destination_position] = uuid; this.save_uuids (); this.items_changed (uint.min (source_position, destination_position), n_changes, n_changes); } public signal void action_added (Ft.Action action); public signal void action_removed (Ft.Action action); public override void dispose () { if (this.settings_changed_id != 0) { this.settings.disconnect (this.settings_changed_id); this.settings_changed_id = 0; } this.uuids = null; this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/action-manager.vala000066400000000000000000000165031520625676500241350ustar00rootroot00000000000000/* * Copyright (c) 2016,2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Glue together actions storage with event bus and logger (for things related to actions). */ [SingleInstance] public class ActionManager : GLib.Object { public Ft.ActionListModel model { get; construct; } private Ft.Logger? logger = null; private Ft.NotificationBackend? notification_backend = null; construct { this.model = new Ft.ActionListModel (); this.model.action_added.connect (this.on_action_added); this.model.action_removed.connect (this.on_action_removed); this.logger = new Ft.Logger (); this.notification_backend = new Ft.NotificationBackend (); this.bind_actions (); } private void foreach_action (GLib.Func func) { if (this.model == null) { return; } var model = this.model; var n_items = model.n_items; for (var position = 0U; position < n_items; position++) { func ((Ft.Action) model.get_item (position)); } } private void bind_action (Ft.Action action) { action.notify["enabled"].connect (this.on_action_notify_enabled); var event_action = action as Ft.EventAction; if (event_action != null) { event_action.triggered.connect (this.on_triggered); } var condition_action = action as Ft.ConditionAction; if (condition_action != null) { condition_action.entered_condition.connect (this.on_entered_condition); condition_action.exited_condition.connect (this.on_exited_condition); } if (action.enabled) { action.bind (); } } private void unbind_action (Ft.Action action) { action.notify["enabled"].disconnect (this.on_action_notify_enabled); var event_action = action as Ft.EventAction; if (event_action != null) { event_action.triggered.disconnect (this.on_triggered); } var condition_action = action as Ft.ConditionAction; if (condition_action != null) { condition_action.entered_condition.disconnect (this.on_entered_condition); condition_action.exited_condition.disconnect (this.on_exited_condition); } action.unbind (); } private void bind_actions () { this.foreach_action ( (action) => { this.bind_action (action); }); } private void unbind_actions () { this.foreach_action ( (action) => { this.unbind_action (action); }); } /** * We don't notify about validation errors */ private void notify_action_failed (Ft.Action action, Ft.CommandExecution execution, ulong entry_id) { var notification = new Ft.Notification ( _("Custom action \"%s\" has failed").printf (action.display_name), execution.error != null ? execution.error.message : ""); notification.event_id = "action-failed"; notification.set_default_action_and_target_value ( "app.log", new GLib.Variant.uint64 ((uint64) entry_id)); // try { // notification.set_icon (GLib.Icon.new_for_string (Config.PACKAGE_NAME)); // } // catch (GLib.Error error) { // GLib.warning (error.message); // } this.notification_backend.send_notification (@"action:$(action.uuid)", notification); } private void watch_action_failed (Ft.Action action, Ft.CommandExecution? execution, ulong entry_id) { if (execution == null) { return; } if (execution.error != null) { this.notify_action_failed (action, execution, entry_id); } else { execution.notify["error"].connect ( () => { this.notify_action_failed (action, execution, entry_id); }); } } private void on_action_notify_enabled (GLib.Object object, GLib.ParamSpec pspec) { var action = (Ft.Action) object; // Sync settings attribute without doing full save action.settings.set_boolean ("enabled", action.enabled); if (action.enabled) { action.bind (); } else { action.unbind (); } // TODO: log action toggled } private void on_triggered (Ft.EventAction action, Ft.Context context, Ft.CommandExecution? execution) { var entry_id = this.logger.log_action_triggered (action, context, execution); this.watch_action_failed (action, execution, entry_id); } private void on_entered_condition (Ft.ConditionAction action, Ft.Context context, Ft.CommandExecution? execution) { var entry_id = this.logger.log_action_entered_condition (action, context, execution); this.watch_action_failed (action, execution, entry_id); } private void on_exited_condition (Ft.ConditionAction action, Ft.Context context, Ft.CommandExecution? execution) { var entry_id = this.logger.log_action_exited_condition (action, context, execution); this.watch_action_failed (action, execution, entry_id); } private void on_action_added (Ft.Action action) { this.bind_action (action); // TODO: log action added } private void on_action_removed (Ft.Action action) { this.unbind_action (action); // TODO: log action removed this.notification_backend.withdraw_notification (@"action:$(action.uuid)"); } public void destroy () { this.unbind_actions (); if (this.model != null) { this.model.action_added.disconnect (this.on_action_added); this.model.action_removed.disconnect (this.on_action_removed); } } public override void dispose () { this.destroy (); // this.model = null; this.logger = null; this.notification_backend = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/action.vala000066400000000000000000000310461520625676500225240ustar00rootroot00000000000000/* * Copyright (c) 2016,2024 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Default timeout in seconds. The intent is to prevent commands from blocking the task queue. */ private const int COMMAND_TIMEOUT = 10; public enum ActionTrigger { EVENT, CONDITION } public abstract class Action : GLib.Object { public string? uuid { get { return this._uuid; } construct { this._uuid = value; } } public GLib.Settings? settings { get { return this._settings; } } [CCode (notify = false)] public bool enabled { get { return this._enabled; } set { if (this._enabled != value) { this._enabled = value; this.notify_property ("enabled"); } } } public string display_name { get; set; default = ""; } private string? _uuid = null; private GLib.Settings? _settings = null; private bool _enabled = true; internal void set_uuid_internal (string uuid) { this._uuid = uuid; } public virtual void load (GLib.Settings settings) { this._settings = settings; this.enabled = settings.get_boolean ("enabled"); this.display_name = settings.get_string ("display-name"); } public virtual void save (GLib.Settings settings) { this._settings = settings; settings.set_boolean ("enabled", this.enabled); settings.set_string ("display-name", this.display_name); } public virtual void bind () { assert_not_reached (); } public virtual void unbind () { assert_not_reached (); } } public class EventAction : Ft.Action { public string[] event_names { get; set; } public Ft.Expression? condition { get; set; } public Ft.Command command { get; set; } public bool wait_for_completion { get; set; default = true; } private Ft.EventBus bus; private uint[] watch_ids; private uint last_context_checksum = 0; construct { this.bus = new Ft.EventBus (); this.watch_ids = new uint[0]; } public EventAction (string? uuid = null) { GLib.Object ( uuid: uuid ); } public override void load (GLib.Settings settings) { base.load (settings); var condition_string = settings.get_string ("condition"); Ft.Expression? condition = null; if (condition_string != "") { var parser = new Ft.ExpressionParser (); try { condition = parser.parse (condition_string); } catch (Ft.ExpressionParserError error) { GLib.warning ("Failed to parse action condition: `%s`", condition_string); } } this.event_names = settings.get_strv ("events"); this.condition = condition; this.wait_for_completion = settings.get_boolean ("wait-for-completion"); this.command = new Ft.Command (settings.get_string ("command")); this.command.working_directory = settings.get_string ("working-directory"); this.command.use_subshell = settings.get_boolean ("use-subshell"); this.command.pass_input = settings.get_boolean ("pass-input"); } public override void save (GLib.Settings settings) { var command = this.command; var condition = this.condition; base.save (settings); settings.set_enum ("trigger", Ft.ActionTrigger.EVENT); settings.set_strv ("events", this.event_names); settings.set_boolean ("wait-for-completion", this.wait_for_completion); if (condition != null) { settings.set_string ("condition", condition.to_string ()); } else { settings.reset ("condition"); } if (command != null) { settings.set_string ("command", ensure_string (command.line)); settings.set_string ("working-directory", ensure_string (command.working_directory)); settings.set_boolean ("use-subshell", command.use_subshell); settings.set_boolean ("pass-input", command.pass_input); } else { settings.reset ("command"); settings.reset ("working-directory"); settings.reset ("use-subshell"); settings.reset ("pass-input"); } } private void on_event (Ft.Event event) { var command = this.command; var context_checksum = event.context.calculate_checksum (); Ft.CommandExecution? execution = null; if (context_checksum == this.last_context_checksum) { // The action may be triggered by several events. Prevent executing the command if the context // hasn't changed. return; } this.last_context_checksum = context_checksum; if (command != null) { if (this.wait_for_completion) { execution = command.prepare (event.context); if (execution != null && !execution.completed) { execution.timeout = COMMAND_TIMEOUT; var queue = new Ft.JobQueue (); queue.push (execution); } } else { execution = command.execute (event.context); } } this.triggered (event.context, execution); } public override void bind () { if (this.enabled) { var condition = this.condition; foreach (var event_name in this.event_names) { var watch_id = this.bus.add_event_watch (event_name, condition, this.on_event); this.watch_ids += watch_id; } assert (this.watch_ids.length == this.event_names.length); } } public override void unbind () { foreach (var watch_id in this.watch_ids) { this.bus.remove_event_watch (watch_id); } this.watch_ids = {}; } public signal void triggered (Ft.Context context, Ft.CommandExecution? execution); } public class ConditionAction : Ft.Action { public Ft.Expression condition { get; set; } public Ft.Command enter_command { get; set; } public Ft.Command exit_command { get; set; } private Ft.EventBus bus; private uint watch_id = 0; construct { this.bus = new Ft.EventBus (); } public ConditionAction (string? uuid = null) { GLib.Object ( uuid: uuid ); } public override void load (GLib.Settings settings) { base.load (settings); var condition_string = settings.get_string ("condition"); Ft.Expression? condition = null; if (condition_string != "") { var parser = new Ft.ExpressionParser (); try { condition = parser.parse (condition_string); } catch (Ft.ExpressionParserError error) { GLib.warning ("Failed to parse action condition: `%s`", condition_string); } } this.condition = condition; this.enter_command = new Ft.Command (settings.get_string ("command")); this.enter_command.working_directory = settings.get_string ("working-directory"); this.enter_command.use_subshell = settings.get_boolean ("use-subshell"); this.enter_command.pass_input = settings.get_boolean ("pass-input"); this.exit_command = new Ft.Command (settings.get_string ("exit-command")); this.exit_command.working_directory = settings.get_string ("working-directory"); this.exit_command.use_subshell = settings.get_boolean ("use-subshell"); this.exit_command.pass_input = settings.get_boolean ("pass-input"); } public override void save (GLib.Settings settings) { var condition = this.condition; var enter_command = this.enter_command; var exit_command = this.exit_command; var any_command = enter_command != null ? enter_command : exit_command; base.save (settings); settings.set_enum ("trigger", Ft.ActionTrigger.CONDITION); if (condition != null) { settings.set_string ("condition", condition.to_string ()); } else { settings.reset ("condition"); } if (enter_command != null) { settings.set_string ("command", ensure_string (enter_command.line)); } else { settings.reset ("command"); } if (exit_command != null) { settings.set_string ("exit-command", ensure_string (exit_command.line)); } else { settings.reset ("exit-command"); } if (any_command != null) { settings.set_string ("working-directory", ensure_string (any_command.working_directory)); settings.set_boolean ("use-subshell", any_command.use_subshell); settings.set_boolean ("pass-input", any_command.pass_input); } else { settings.reset ("working-directory"); settings.reset ("use-subshell"); settings.reset ("pass-input"); } } private void on_enter_condition (Ft.Context context) { var command = this.enter_command; Ft.CommandExecution? execution = null; if (command != null) { execution = command.prepare (context); if (execution != null && !execution.completed) { execution.timeout = COMMAND_TIMEOUT; var queue = new Ft.JobQueue (); queue.push (execution); } } this.entered_condition (context, execution); } private void on_exit_condition (Ft.Context context) { var command = this.exit_command; Ft.CommandExecution? execution = null; if (command != null) { execution = command.prepare (context); if (execution != null && !execution.completed) { execution.timeout = COMMAND_TIMEOUT; var queue = new Ft.JobQueue (); queue.push (execution); } } this.exited_condition (context, execution); } public override void bind () { if (this.enabled && this.condition != null && this.watch_id == 0) { this.watch_id = this.bus.add_condition_watch (this.condition, this.on_enter_condition, this.on_exit_condition); } } public override void unbind () { if (this.watch_id != 0) { this.bus.remove_condition_watch (this.watch_id); this.watch_id = 0; } } public signal void entered_condition (Ft.Context context, Ft.CommandExecution? execution); public signal void exited_condition (Ft.Context context, Ft.CommandExecution? execution); } } focustimerhq-FocusTimer-8581be2/src/core/background-manager.vala000066400000000000000000000247151520625676500250030ustar00rootroot00000000000000/* * Copyright (c) 2025-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public interface BackgroundProvider : Ft.Provider { public abstract bool background_allowed { get; } public abstract bool autostart_allowed { get; } public abstract async void request_background (bool autostart, string parent_window); } /** * A fallback implementation for missing Background portal. * * `BackgroundManager` already handles the application hold. Only thing to do * is managing the autostart file. */ private class DefaultBackgroundProvider : Ft.Provider, Ft.BackgroundProvider { private const string AUTOSTART_TEMPLATE = """[Desktop Entry] Type=Application Name=${APPLICATION_ID} X-XDP-Autostart=${APPLICATION_ID} Exec=focus-timer --gapplication-service """; public bool background_allowed { get { return this.available; } } public bool autostart_allowed { get { return this._autostart_allowed; } } private bool _autostart_allowed = false; private GLib.File get_autostart_file () { var path = GLib.Path.build_filename ( GLib.Environment.get_user_config_dir (), "autostart", @"$(Config.APPLICATION_ID).desktop"); return GLib.File.new_for_path (path); } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.available = !Ft.is_flatpak (); } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { var autostart_file = this.get_autostart_file (); var autostart_allowed = false; try { yield autostart_file.query_info_async ( GLib.FileAttribute.STANDARD_TYPE, GLib.FileQueryInfoFlags.NONE, GLib.Priority.DEFAULT, cancellable); autostart_allowed = true; } catch (GLib.IOError.NOT_FOUND error) { } catch (GLib.Error error) { GLib.warning ("Failed to query autostart file: %s", error.message); } this._autostart_allowed = autostart_allowed; this.notify_property ("autostart-allowed"); } public override async void disable () throws GLib.Error { } public override async void uninitialize () throws GLib.Error { } public async void request_background (bool autostart, string parent_window) { var autostart_file = this.get_autostart_file (); var autostart_contents = AUTOSTART_TEMPLATE.replace ( "${APPLICATION_ID}", Config.APPLICATION_ID); try { if (autostart) { yield autostart_file.replace_contents_async ( autostart_contents.data, null, false, GLib.FileCreateFlags.NONE, null, null); } else { yield autostart_file.delete_async (); } } catch (GLib.IOError.NOT_FOUND error) { // already removed } catch (GLib.Error error) { GLib.warning ("Failed to update autostart file: %s", error.message); return; } this._autostart_allowed = autostart; this.notify_property ("autostart-allowed"); } } [SingleInstance] public class BackgroundManager : Ft.ProvidedObject { public bool active { get { return this.has_application_hold; } } public bool background_allowed { get { return this._background_allowed; } } public bool autostart_allowed { get { return this._autostart_allowed; } } private unowned GLib.Application? application = null; private GLib.Settings? settings = null; private bool has_application_hold = false; private GLib.GenericSet holds = null; private static uint next_hold_id = 1U; private bool _background_allowed = false; private bool _autostart_allowed = false; private async void request_background (string parent_window) { if (this.provider == null || !this.provider.enabled) { return; } this.application.hold (); yield this.provider.request_background (this.settings.get_boolean ("autostart"), parent_window); this.update_application_hold (); this.application.release (); } private void hold_application () { if (!this.has_application_hold) { this.application.hold (); this.has_application_hold = true; } } private void release_application () { if (this.has_application_hold) { this.application.release (); this.has_application_hold = false; } } private void update_application_hold () { var is_provider_enabled = this.provider != null && this.provider.enabled; if (this.holds.length > 0U && (this._background_allowed || !is_provider_enabled)) { this.hold_application (); } else { this.release_application (); } } public async uint hold (string parent_window = "") { var hold_id = Ft.BackgroundManager.next_hold_id; BackgroundManager.next_hold_id++; this.holds.add (hold_id); this.hold_application (); yield this.request_background (parent_window); return hold_id; } public uint hold_sync (string parent_window = "") { var hold_id = Ft.BackgroundManager.next_hold_id; Ft.BackgroundManager.next_hold_id++; this.holds.add (hold_id); this.hold_application (); this.request_background.begin (parent_window); return hold_id; } public void release (uint hold_id) { var removed = this.holds.remove (hold_id); if (removed) { this.update_application_hold (); } } private void update_properties () { var provider = this.provider; var background_allowed = provider != null ? provider.background_allowed : false; var autostart_allowed = provider != null ? provider.autostart_allowed : false; if (this._background_allowed != background_allowed) { this._background_allowed = background_allowed; this.notify_property ("background-allowed"); } if (this._autostart_allowed != autostart_allowed) { this._autostart_allowed = autostart_allowed; this.notify_property ("autostart-allowed"); } } protected override void initialize () { this.application = GLib.Application.get_default (); this.holds = new GLib.GenericSet (GLib.direct_hash, GLib.direct_equal); this.settings = Ft.get_settings (); this.settings.changed.connect (this.on_settings_changed); } protected override void setup_providers () { this.providers.add (new Ft.DefaultBackgroundProvider ()); } protected override void provider_enabled (Ft.BackgroundProvider provider) { provider.notify["background-allowed"].connect (this.on_notify_background_allowed); provider.notify["autostart-allowed"].connect (this.on_notify_autostart_allowed); this.update_properties (); if (this.holds.length > 0U || this.settings.get_boolean ("autostart")) { this.request_background.begin (""); } } protected override void provider_disabled (Ft.BackgroundProvider provider) { // TODO: use SetStatus to withdraw request? provider.notify["background-allowed"].disconnect (this.on_notify_background_allowed); provider.notify["autostart-allowed"].disconnect (this.on_notify_autostart_allowed); this.update_properties (); this.release_application (); } private void on_notify_background_allowed (GLib.Object object, GLib.ParamSpec pspec) { this.update_properties (); } private void on_notify_autostart_allowed (GLib.Object object, GLib.ParamSpec pspec) { this.update_properties (); } private void on_settings_changed (GLib.Settings settings, string key) { switch (key) { case "autostart": if (settings.get_boolean (key) != this._autostart_allowed) { this.request_background.begin (""); } break; } } public void destroy () { this.holds?.remove_all (); this.release_application (); } public override void dispose () { this.destroy (); if (this.settings != null) { this.settings.changed.disconnect (this.on_settings_changed); this.settings = null; } this.application = null; this.holds = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/command.vala000066400000000000000000000557041520625676500226740ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { private struct CommandVariable { public string name_raw; public string name; public string format_raw; public string format; public int span_start; public int span_end; public int arg_index; public CommandVariable () { this.span_start = -1; this.span_end = -1; } } private void find_variables (string text, GLib.Func callback) { unichar chr; int chr_span_start = 0; int chr_span_end = 0; var variable = CommandVariable (); var within_brackets = false; while (text.get_next_char (ref chr_span_end, out chr)) { if (variable.span_start < 0) { if (chr == '$') { variable.span_start = chr_span_start; } } else { if (!within_brackets) { if (chr == '{' && chr_span_start == variable.span_start + 1) { within_brackets = true; } else if (!chr.isalnum ()) { variable.span_end = chr_span_start; variable.name_raw = text.slice ((long) variable.span_start + 1, (long) variable.span_end); variable.name = from_camel_case (variable.name_raw); variable.format_raw = ""; variable.format = ""; if (variable.name != "") { callback (variable); } variable = CommandVariable (); } } else if (chr == '}') { variable.span_end = chr_span_end; var bracket_text = text.slice ((long) variable.span_start + 2, (long) variable.span_end - 1); var bracket_tokens = bracket_text.split (":", 2); if (bracket_tokens.length == 2) { variable.name_raw = bracket_tokens[0].strip (); variable.name = from_camel_case (variable.name_raw); variable.format_raw = bracket_tokens[1].strip (); variable.format = from_camel_case (variable.format_raw); } else { variable.name_raw = bracket_text.strip (); variable.name = from_camel_case (variable.name_raw); variable.format_raw = ""; variable.format = ""; } callback (variable); within_brackets = false; variable = CommandVariable (); } } chr_span_start = chr_span_end; } if (variable.span_start >= 0 && !within_brackets) { variable.span_end = chr_span_end; variable.name = text.slice ((long) variable.span_start + 1, (long) variable.span_end); variable.format = ""; if (variable.name != "") { callback (variable); } } } private string? find_program_in_host_path (string program) { if (program == null) { return null; } if (Ft.is_flatpak ()) { string standard_output; int wait_status; try { GLib.Process.spawn_sync (null, { "flatpak-spawn", "--host", "which", program }, null, GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.STDERR_TO_DEV_NULL, setup_child, out standard_output, null, out wait_status); if (wait_status == 0 && standard_output != null && standard_output.length > 0) { return standard_output; } } catch (GLib.SpawnError error) { GLib.warning ("Error finding program: %s", error.message); } return null; } else { return GLib.Environment.find_program_in_path (program); } } private void setup_child () { // Create new session to detach from tty, but set a process. Posix.setsid (); // Group so all children can be ḱilled if need be. Posix.setpgid (0, 0); } public errordomain CommandError { EMPTY_LINE, SYNTAX_ERROR, UNKNOWN_VARIABLE, UNKNOWN_VARIABLE_FORMAT, NOT_FOUND, FAILED } /** * Object representing a single execution of a command - its arguments and output. * * It's an object because we may display it in the Log UI and updates are applied on its own. */ public class CommandExecution : GLib.Object, Ft.Job { private const GLib.SpawnFlags SPAWN_FLAGS = GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.LEAVE_DESCRIPTORS_OPEN | GLib.SpawnFlags.DO_NOT_REAP_CHILD; public string[] args { get; construct; } public string? working_directory { get; construct; } public string input { get; set; default = ""; } public int timeout { get; set; default = -1; } public int exit_code { get; private set; default = -1; } public string output { get; private set; default = ""; } public int64 execution_time { get; private set; default = 0; } [CCode (notify = false)] public bool completed { get { return this._completed; } set { if (this._completed != value) { this._completed = value; this.notify_property ("completed"); } } } public GLib.Error? error { get { return this._error; } set { this._error = value; } } private GLib.Cancellable? cancellable = null; private GLib.Error? _error = null; private bool _completed = false; public CommandExecution (string[] args, string? working_directory) { GLib.Object ( args: args, working_directory: working_directory ); } private static bool process_output_line (GLib.IOChannel channel, GLib.IOCondition condition, GLib.StringBuilder output) { if (condition == GLib.IOCondition.HUP) { return false; } try { string line; channel.read_line (out line, null, null); output.append (line); } catch (GLib.IOChannelError error) { GLib.debug ("IOChannelError: %s", error.message); return false; } catch (GLib.ConvertError error) { GLib.debug ("ConvertError: %s", error.message); return false; } return true; } /** * TODO: Currently there may be several executions ongoing for a command - it should always * execute once at a time */ public async bool run () throws GLib.Error requires (this.cancellable == null) { if (this._error != null) { throw this._error.copy (); } GLib.SourceFunc? callback = null; GLib.Pid child_pid; int standard_input; int standard_output; int standard_error; uint timeout_id = 0; string[] args; var timeout = this.timeout; var working_directory = this.working_directory; var output_builder = new GLib.StringBuilder (); this.cancellable = new GLib.Cancellable (); try { if (Ft.is_flatpak ()) { string[] flatpak_spawn_args = { "flatpak-spawn", "--host" }; if (working_directory != null) { flatpak_spawn_args += @"--directory=$(working_directory)"; working_directory = null; } foreach (var arg in this.args) { flatpak_spawn_args += arg; } args = flatpak_spawn_args; } else { args = this.args; } var start_time = GLib.get_monotonic_time (); GLib.Process.spawn_async_with_pipes (working_directory, args, null, SPAWN_FLAGS, setup_child, out child_pid, out standard_input, out standard_output, out standard_error); if (this.input != "") { var input_channel = new GLib.IOChannel.unix_new (standard_input); try { input_channel.write_chars (this.input.to_utf8 (), null); } catch (GLib.Error error) { GLib.warning ("Failed to pass JSON data: %s", error.message); } input_channel.shutdown (true); } var output_channel = new GLib.IOChannel.unix_new (standard_output); output_channel.add_watch ( GLib.IOCondition.IN | GLib.IOCondition.HUP, (channel, condition) => { return process_output_line (channel, condition, output_builder); }); var error_channel = new GLib.IOChannel.unix_new (standard_error); error_channel.add_watch ( GLib.IOCondition.IN | GLib.IOCondition.HUP, (channel, condition) => { return process_output_line (channel, condition, output_builder); }); GLib.ChildWatch.add ( child_pid, (pid, status) => { // Check the exit status of the child process try { GLib.Process.check_wait_status (status); this.exit_code = 0; } catch (GLib.Error error) { this.exit_code = error.code; if (this.error == null) { var command_line = string.joinv (" ", this.args); GLib.warning ("Spawned command '%s' exited abnormally: %s", command_line, error.message); this.error = error.copy (); } } finally { this.execution_time = GLib.get_monotonic_time () - start_time; this.output = output_builder.str; } if (callback != null) { callback (); } }); callback = run.callback; if (timeout > 0) { timeout_id = GLib.Timeout.add_seconds ( timeout, () => { timeout_id = 0; this.error = new Ft.CommandError.FAILED (_("Reached timeout")); this.cancellable.cancel (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (timeout_id, "Ft.CommandExecution.run"); } this.cancellable.cancelled.connect ( () => { if (this.exit_code < 0) { var command_line = string.joinv (" ", this.args); GLib.debug ("Cancel command `%s` with pid=%d", command_line, child_pid); Posix.kill (child_pid, Posix.Signal.TERM); } }); yield; GLib.Process.close_pid (child_pid); // TODO: are file descriptors closed properly? } catch (GLib.SpawnError error) { var command_line = string.joinv (" ", this.args); GLib.warning ("Error while spawning command `%s`: %s", command_line, error.message); this.error = new Ft.CommandError.FAILED (_("Failed to execute command")); throw this.error; } finally { if (timeout_id != 0) { GLib.Source.remove (timeout_id); timeout_id = 0; } this.completed = true; } return this.error == null && this.exit_code == 0; } public string get_line () { var line = new GLib.StringBuilder (); var index = 0; foreach (var arg in this.args) { if (index > 0) { line.append_c (' '); } line.append (arg); // TODO: escape / quote string index++; } return line.str; } } public class Command : GLib.Object { public string line { get { return this._line; } set { if (this._line != value) { this._line = value; this.prepared = false; } } } public bool use_subshell { get { return this._use_subshell; } set { if (this._use_subshell != value) { this._use_subshell = value; this.prepared = false; } } } public string working_directory { get; set; } public bool pass_input { get; set; default = false; } private string _line; private bool _use_subshell = false; private bool prepared = false; private string[] args; private CommandVariable[] variables; public Command (string line) { GLib.Object ( line: line ); } private void prepare_args () throws Ft.CommandError { if (this._use_subshell) { var line = this._line.strip (); if (line.length == 0) { throw new Ft.CommandError.EMPTY_LINE (_("Command is empty")); } // We don't have a way to validate the shell command, so use it as is. this.args = { "sh", "-c", line }; } else { try { GLib.Shell.parse_argv (this._line, out this.args); } catch (GLib.ShellError error) { this.args = { line }; if (error is GLib.ShellError.EMPTY_STRING) { throw new Ft.CommandError.EMPTY_LINE (_("Command is empty")); } if (error is GLib.ShellError.BAD_QUOTING) { throw new Ft.CommandError.SYNTAX_ERROR (_("Unclosed quotation mark")); } GLib.warning ("Unexpected error while parsing command '%s': %s", this._line, error.message); throw new Ft.CommandError.SYNTAX_ERROR (_("Invalid command")); } } } private void prepare_variables () throws Ft.CommandError requires (this.args != null) { var variables = new CommandVariable[0]; for (var index = 0; index < this.args.length; index++) { Ft.CommandError? error = null; find_variables ( this.args[index], (variable) => { if (error != null) { return; } if (variable.name != "") { if (Ft.find_variable (variable.name) == null) { error = new Ft.CommandError.UNKNOWN_VARIABLE ( _("Unknown variable \"%s\""), variable.name_raw); return; } if (!Ft.find_variable_format (variable.name, variable.format)) { error = new Ft.CommandError.UNKNOWN_VARIABLE_FORMAT ( _("Unknown format \"%s\""), variable.format_raw); return; } } variable.arg_index = index; variables += variable; }); if (error != null) { throw error; } } this.variables = variables; } private string[] interpolate_args (Ft.Context context) requires (this.args != null) { var args = this.args.copy (); for (var index = this.variables.length - 1; index >= 0; index--) { var variable = this.variables[index]; var variable_value = context.evaluate_variable (variable.name); var variable_string = ""; if (variable_value != null) { try { var variable_variant = variable_value.format (variable.format); variable_string = variable_variant.is_of_type (GLib.VariantType.STRING) ? variable_variant.get_string () : variable_variant.print (false); } catch (Ft.ExpressionError error) { // Error should be cough earlier, during prepare. } } args[variable.arg_index] = args[variable.arg_index].splice (variable.span_start, variable.span_end, variable_string); } return args; } private void prepare_internal () throws Ft.CommandError { if (!this.prepared) { this.prepare_args (); this.prepare_variables (); this.prepared = true; } else { assert (this.args != null); } } public void validate () throws Ft.CommandError { this.prepare_internal (); // TODO: we don't validate shell/bash syntax, make custom command-line parser if (this.args.length != 0 && !this._use_subshell) { var program_path = find_program_in_host_path (this.args[0]); if (program_path == null) { throw new Ft.CommandError.NOT_FOUND (_("Program \"%s\" not found"), this.args[0]); } } // TODO: validate working directory } public Ft.CommandExecution? prepare (Ft.Context context) // TODO: make it private or remove; just execute and allow to cancel job { Ft.CommandExecution? execution = null; try { this.prepare_internal (); execution = new Ft.CommandExecution (this.interpolate_args (context), this.working_directory); if (this.pass_input) { execution.input = context.to_json (); } } catch (Ft.CommandError error) { if (!(error is Ft.CommandError.EMPTY_LINE)) { execution = new Ft.CommandExecution (this.args, this.working_directory); execution.error = error; execution.completed = true; } } return execution; } public Ft.CommandExecution? execute (Ft.Context context) { var execution = this.prepare (context); if (execution != null && !execution.completed) { execution.run.begin ( (obj, res) => { try { execution.run.end (res); } catch (GLib.Error error) { execution.error = error; } }); } return execution; } public async Ft.CommandExecution? execute_async (Ft.Context context) { var execution = this.prepare (context); if (execution != null && execution.error == null) { try { yield execution.run (); } catch (GLib.Error error) { execution.error = error; } } return execution; } } } focustimerhq-FocusTimer-8581be2/src/core/context.vala000066400000000000000000000166301520625676500227350ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Snapshot of timer/session state at a given time. */ public class Context // TODO: make it compact class? { public string? event_source; public int64 timestamp; public Ft.TimerState timer_state; public Ft.TimeBlock? time_block; public Ft.Session? session; private string? json = null; private static string current_event_source = null; private static int64 current_event_source_timestamp = Ft.Timestamp.UNDEFINED; public Context.build (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); Ft.Variables.ensure_initialized (); var session_manager = Ft.SessionManager.get_default (); var timer = session_manager.timer; if (Ft.Timestamp.is_undefined (current_event_source_timestamp)) { current_event_source_timestamp = timestamp; } else if (current_event_source_timestamp != timestamp) { unset_event_source (); } this.event_source = current_event_source; this.timestamp = timestamp; this.timer_state = timer.state; this.time_block = session_manager.current_time_block; this.session = session_manager.current_session; } // TODO: Calculate Murmur3 hash or serialize this as string public uint calculate_checksum () { return GLib.str_hash (this.event_source != null ? @"$(this.event_source) $(this.timestamp)" : this.timestamp.to_string ()); } /* * Event Source */ public static string? get_event_source () { return current_event_source; } public static int64 get_event_source_timestamp () { return current_event_source_timestamp; } public static void set_event_source (string event_source, int64 timestamp = Ft.Timestamp.UNDEFINED) { current_event_source = event_source; current_event_source_timestamp = timestamp; } public static void unset_event_source () { current_event_source = null; current_event_source_timestamp = Ft.Timestamp.UNDEFINED; } /* * JSON representation */ private void json_add_timer_state (Json.Builder builder) { builder.begin_object (); builder.set_member_name ("duration"); builder.add_int_value (this.timer_state.duration); builder.set_member_name ("offset"); builder.add_int_value (this.timer_state.offset); builder.set_member_name ("startedTime"); builder.add_int_value (this.timer_state.started_time); builder.set_member_name ("pausedTime"); builder.add_int_value (this.timer_state.paused_time); builder.set_member_name ("finishedTime"); builder.add_int_value (this.timer_state.finished_time); builder.set_member_name ("isRunning"); builder.add_boolean_value (this.timer_state.is_running ()); builder.set_member_name ("elapsed"); builder.add_int_value (this.timer_state.calculate_elapsed (this.timestamp)); builder.set_member_name ("remaining"); builder.add_int_value (this.timer_state.calculate_remaining (this.timestamp)); builder.end_object (); } private void json_add_time_block (Json.Builder builder, Ft.TimeBlock? time_block) { if (time_block == null) { builder.add_null_value (); return; } builder.begin_object (); // TODO // builder.set_member_name ("id"); // builder.add_int_value (time_block.id); builder.set_member_name ("startTime"); builder.add_int_value (time_block.start_time); builder.set_member_name ("endTime"); builder.add_int_value (time_block.end_time); builder.set_member_name ("state"); builder.add_string_value (time_block.state.to_string ()); builder.set_member_name ("status"); builder.add_string_value (time_block.get_status ().to_string ()); builder.set_member_name ("gaps"); builder.begin_array (); time_block.foreach_gap ( (gap) => { builder.begin_object (); builder.set_member_name ("startTime"); builder.add_int_value (gap.start_time); builder.set_member_name ("endTime"); builder.add_int_value (gap.end_time); // TODO // builder.set_member_name ("reason"); // builder.add_int_value (gap.reason.to_string (); builder.end_object (); }); builder.end_array (); builder.end_object (); } public string to_json () { if (this.json == null) { var builder = new Json.Builder (); builder.begin_object(); builder.set_member_name ("timestamp"); builder.add_int_value (this.timestamp); // TODO: if (Config.DEBUG) { builder.set_member_name ("_source"); builder.add_string_value (ensure_string (this.event_source)); // } // Timer state builder.set_member_name ("timer"); this.json_add_timer_state (builder); // Current session builder.set_member_name ("session"); builder.begin_array (); if (this.session != null) { this.session.@foreach ( (time_block) => { // Hide scheduled time-blocks when the timer has stopped. This data can be misleading. // We need those blocks to display session indicator, but here they don't make sense. if (this.time_block == null && time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED) { return; } this.json_add_time_block (builder, time_block); }); } builder.end_array (); builder.end_object (); var generator = new Json.Generator (); generator.set_root (builder.get_root ()); generator.set_pretty (true); this.json = generator.to_data (null); } return this.json; } /* * Variables */ public Ft.Value? evaluate_variable (string variable_name) { unowned var variable_spec = Ft.find_variable (variable_name); return variable_spec != null ? variable_spec.evaluate (this) : null; } } } focustimerhq-FocusTimer-8581be2/src/core/cycle.vala000066400000000000000000000356221520625676500223520ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * A convenience class describing a cycle. * * Cycle consists of a single pomodoro and breaks following it. In case pomodoro comes right after another pomodoro * we treat it as new cycle to make session indicator more readable, although it's not true to a definition of * a cycle. Uncompleted time-blocks are included, therefore some cycles may be considered as invalid. * * Cycle does not serve as a container for time-blocks, its more like an annotation. */ public class Cycle : GLib.Object { public unowned Ft.Session session { get { unowned GLib.List link = this.time_blocks.first (); return link != null ? link.data.session : null; } } public int64 start_time { get { return this._start_time; } } public int64 end_time { get { return this._end_time; } } public int64 duration { get { return Ft.Timestamp.subtract (this._end_time, this._start_time); } } private GLib.List time_blocks; private int64 _start_time = Ft.Timestamp.UNDEFINED; private int64 _end_time = Ft.Timestamp.UNDEFINED; private int changed_freeze_count = 0; private bool changed_is_pending = false; private double progress_value = double.NAN; private int64 progress_reference_start_time = Ft.Timestamp.UNDEFINED; private int64 progress_reference_end_time = Ft.Timestamp.UNDEFINED; // Metadata private double weight = double.NAN; private int64 completion_time = Ft.Timestamp.UNDEFINED; construct { this.time_blocks = new GLib.List (); } private void on_time_block_changed () { this.emit_changed (); } private void emit_added (Ft.TimeBlock time_block) { time_block.changed.connect (this.on_time_block_changed); this.added (time_block); } private void emit_removed (Ft.TimeBlock time_block) { time_block.changed.disconnect (this.on_time_block_changed); this.removed (time_block); } private void emit_changed () { if (this.changed_freeze_count > 0) { this.changed_is_pending = true; } else { this.changed_is_pending = false; this.changed (); } } public void freeze_changed () { this.changed_freeze_count++; } public void thaw_changed () { this.changed_freeze_count--; if (this.changed_freeze_count == 0 && this.changed_is_pending) { this.emit_changed (); } } private void update_time_range () { unowned Ft.TimeBlock first_time_block = this.get_first_time_block (); unowned Ft.TimeBlock last_time_block = this.get_last_time_block (); var old_duration = this._end_time - this._start_time; var start_time = first_time_block != null ? first_time_block.start_time : Ft.Timestamp.UNDEFINED; var end_time = last_time_block != null ? last_time_block.end_time : Ft.Timestamp.UNDEFINED; if (this._start_time != start_time) { this._start_time = start_time; this.notify_property ("start-time"); } if (this._end_time != end_time) { this._end_time = end_time; this.notify_property ("end-time"); } if (this._end_time - this._start_time != old_duration) { this.notify_property ("duration"); } } private void remove_link (GLib.List? link) { if (link == null) { return; } var time_block = link.data; link.data = null; this.time_blocks.delete_link (link); this.emit_removed (time_block); } public void remove (Ft.TimeBlock time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link != null) { this.remove_link (link); } else { GLib.warning ("Ignoring `Cycle.remove()`. Time-block does not belong to the cycle."); } } public void append (Ft.TimeBlock time_block) { this.time_blocks.append (time_block); this.emit_added (time_block); } public unowned Ft.TimeBlock? get_first_time_block () { unowned GLib.List link = this.time_blocks.first (); return link != null ? link.data : null; } public unowned Ft.TimeBlock? get_last_time_block () { unowned GLib.List link = this.time_blocks.last (); return link != null ? link.data : null; } public bool contains (Ft.TimeBlock time_block) { unowned GLib.List link = this.time_blocks.first (); while (link != null) { if (link.data == time_block) { return true; } link = link.next; } return false; } public void @foreach (GLib.Func func) { unowned GLib.List link = this.time_blocks.first (); while (link != null) { func (link.data); link = link.next; } } /* * Functions for metadata */ public double get_weight () { if (this.weight.is_nan ()) { unowned GLib.List link = this.time_blocks.first (); var weight = 0.0; while (link != null) { if (link.data.get_status () != Ft.TimeBlockStatus.UNCOMPLETED) { var time_block_weight = link.data.get_weight (); weight = !time_block_weight.is_nan () ? weight + double.max (time_block_weight, 0.0) : double.NAN; } link = link.next; } this.weight = weight; } return this.weight; } public int64 get_completion_time () { if (Ft.Timestamp.is_undefined (this.completion_time)) { unowned GLib.List link = this.time_blocks.first (); var completion_time = Ft.Timestamp.UNDEFINED; var time_block_completion_time = Ft.Timestamp.UNDEFINED; while (link != null) { if (link.data.get_status () != Ft.TimeBlockStatus.UNCOMPLETED && link.data.get_weight () > 0.0) { time_block_completion_time = link.data.get_completion_time (); completion_time = Ft.Timestamp.is_defined (time_block_completion_time) ? time_block_completion_time : link.data.end_time; } link = link.next; } this.completion_time = completion_time; } return this.completion_time; } public bool is_scheduled () { unowned GLib.List link = this.time_blocks.first (); var first_status = link != null ? link.data.get_status () : Ft.TimeBlockStatus.SCHEDULED; return first_status == Ft.TimeBlockStatus.SCHEDULED; } public Ft.TimeBlockStatus get_status () { unowned GLib.List link = this.time_blocks.first (); if (link == null || link.data.get_status () == Ft.TimeBlockStatus.SCHEDULED) { return Ft.TimeBlockStatus.SCHEDULED; } while (link != null) { if (link.data.get_status () == Ft.TimeBlockStatus.IN_PROGRESS || link.data.get_status () == Ft.TimeBlockStatus.SCHEDULED) { return Ft.TimeBlockStatus.IN_PROGRESS; } link = link.next; } return this.is_visible () ? Ft.TimeBlockStatus.COMPLETED : Ft.TimeBlockStatus.UNCOMPLETED; } /** * Hide cycles that were uncompleted. */ public bool is_visible () { unowned GLib.List link = this.time_blocks.first (); while (link != null) { if (link.data.get_status () != Ft.TimeBlockStatus.UNCOMPLETED && link.data.get_weight () > 0.0) { return true; } link = link.next; } return false; } public void prepare_progress (int64 timestamp) { unowned GLib.List link = this.time_blocks.first (); var progress = 0.0; var total_weight = 0.0; var in_progress = false; while (link != null) { var time_block_weight = link.data.get_weight (); var last_gap = link.data.get_last_gap (); if (link.data.get_status () != Ft.TimeBlockStatus.UNCOMPLETED) { total_weight += time_block_weight; } if (link.data.get_status () == Ft.TimeBlockStatus.IN_PROGRESS) { in_progress = true; } if (last_gap != null && Ft.Timestamp.is_undefined (last_gap.end_time)) { in_progress = false; } progress += time_block_weight * link.data.calculate_progress (timestamp); link = link.next; } if (total_weight != 0.0) { progress /= total_weight; } if (!in_progress || progress >= 1.0) { this.progress_value = progress.clamp (0.0, 1.0); this.progress_reference_start_time = Ft.Timestamp.UNDEFINED; this.progress_reference_end_time = Ft.Timestamp.UNDEFINED; } else { var completion_time = this.get_completion_time (); this.progress_value = double.NAN; this.progress_reference_start_time = (int64)( ((double) timestamp - progress * (double) completion_time) / (1.0 - progress)); this.progress_reference_end_time = completion_time; if (this.progress_reference_start_time >= this.progress_reference_end_time) { this.progress_value = progress.clamp (0.0, 1.0); this.progress_reference_start_time = Ft.Timestamp.UNDEFINED; this.progress_reference_end_time = Ft.Timestamp.UNDEFINED; } } } /** * Calculate cycle progress. * * Uses cache to make following calls cheaper to estimate. */ public double calculate_progress (int64 timestamp) { if (Ft.Timestamp.is_undefined (this._end_time) || Ft.Timestamp.is_undefined (this._start_time)) { return 0.0; } Ft.ensure_timestamp (ref timestamp); if (Ft.Timestamp.is_undefined (this.progress_reference_start_time) && this.progress_value.is_nan ()) { this.prepare_progress (timestamp); } if (!this.progress_value.is_nan ()) { return this.progress_value; } if (this.progress_reference_start_time >= timestamp) { return 0.0; } if (this.progress_reference_end_time <= timestamp) { return 1.0; } return ( (double)(timestamp - this.progress_reference_start_time) / (double)(this.progress_reference_end_time - this.progress_reference_start_time) ).clamp (0.0, 1.0); } public int64 calculate_progress_duration (int64 timestamp) // XXX: bad name { if (Ft.Timestamp.is_undefined (this.progress_reference_start_time)) { Ft.ensure_timestamp (ref timestamp); this.prepare_progress (timestamp); } return Ft.Timestamp.subtract (this.progress_reference_end_time, this.progress_reference_start_time); } /** * Invalidate cache. Cache helps avoiding re-iterating time-blocks for certain operations. * * You should call it on every time-block change or change of metadata. */ private void invalidate_cache () { this.weight = double.NAN; this.completion_time = Ft.Timestamp.UNDEFINED; this.progress_value = double.NAN; this.progress_reference_start_time = Ft.Timestamp.UNDEFINED; this.progress_reference_end_time = Ft.Timestamp.UNDEFINED; } public override void dispose () { unowned GLib.List link; while ((link = this.time_blocks.first ()) != null) { link.data.changed.disconnect (this.on_time_block_changed); link.data = null; this.time_blocks.delete_link (link); } base.dispose (); } /* * Signals */ [Signal (run = "last")] public signal void added (Ft.TimeBlock time_block) { this.emit_changed (); } [Signal (run = "last")] public signal void removed (Ft.TimeBlock time_block) { this.emit_changed (); } [Signal (run = "first")] public signal void changed () { this.invalidate_cache (); this.update_time_range (); } } } focustimerhq-FocusTimer-8581be2/src/core/database.vala000066400000000000000000000412021520625676500230060ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft.Database { private const uint VERSION = 3; private const string MIGRATIONS_URI = "resource:///io/github/focustimerhq/FocusTimer/migrations"; private const string DATE_FORMAT = "%Y-%m-%d"; private Gom.Adapter? adapter = null; private Gom.Repository? repository = null; public unowned Gom.Repository? get_repository () { return Ft.Database.repository; } private void make_directory_with_parents (GLib.File directory) { if (!directory.query_exists ()) { try { directory.make_directory_with_parents (); } catch (GLib.Error error) { GLib.warning ("Failed to create directory: %s", error.message); } } } /** * Convenience function to execute multiline SQL. * * `Gom.Adapter.execute_sql` is limited to single line queries. * * This MUST be called from within a write transaction using * Gom.Adapter.queue_write(). */ private void execute_sql (Gom.Adapter adapter, string sql) throws GLib.Error { unowned var database = adapter.get_handle (); string error_message; if (database.exec (sql, null, out error_message) != Sqlite.OK) { throw new Gom.Error.COMMAND_SQLITE (error_message); } } private bool is_migration_applied (Gom.Repository repository, Gom.Adapter adapter, uint version) { try { Gom.Cursor? cursor = null; var check_command = (Gom.Command) GLib.Object.@new (typeof (Gom.Command), adapter: adapter); check_command.set_sql ("SELECT 1 FROM _gom_version WHERE version = ? LIMIT 1;"); check_command.set_param_uint (0U, version); check_command.execute (out cursor); if (cursor != null && cursor.next ()) { return true; } } catch (GLib.Error error) { } return false; } // public because it's used in tests public bool migrate_repository (Gom.Repository repository, Gom.Adapter adapter, uint version) throws GLib.Error { var is_test = Ft.is_test (); // Gom tries to re-apply last migration (bug in Gom?). if (is_migration_applied (repository, adapter, version)) { if (!is_test) { GLib.info ("Migration version %u already applied, skipping", version); } return true; } uint8[] file_contents; var file = File.new_for_uri (@"$(MIGRATIONS_URI)/version-$(version).sql"); file.load_contents (null, out file_contents, null); try { if (!is_test) { GLib.info ("Migrating database to version %u", version); } Ft.Database.execute_sql (adapter, (string) file_contents); } catch (GLib.Error error) { throw error; } return true; } /** * Check database health using PRAGMA `quick_check`. * * This function internally uses queue_read to run the check in the * GOM worker thread, then blocks the calling thread until the result * is available (similar to how `open_sync` / `close_sync` work). * * `quick_check` is faster than `integrity_check`. Should be good enough for most cases. */ private bool check_health (Gom.Adapter adapter) { var result_queue = new GLib.AsyncQueue (); adapter.queue_read (() => { var is_healthy = true; // be forgiving by default unowned Sqlite.Database db = adapter.get_handle (); Sqlite.Statement statement; if (db.prepare_v2 ("PRAGMA quick_check;", -1, out statement) != Sqlite.OK) { GLib.warning ("Failed to prepare database health check: %s", db.errmsg ()); } else if (statement.step () != Sqlite.ROW) { GLib.warning ("Database health check returned no results."); } else { unowned var result = statement.column_text (0); if (result == "ok") { is_healthy = true; } else { GLib.warning ("Database health check failed: %s", result); is_healthy = false; } } result_queue.push (is_healthy); }); // Block until result is available return result_queue.pop (); } private void rename_corrupted_file (GLib.File database_file) throws GLib.Error { var stamp = new GLib.DateTime.now_local ().format ("%Y-%m-%d"); var destination_path = @"$(database_file.get_path()).corrupted-$(stamp)"; var destination_file = GLib.File.new_for_path (destination_path); database_file.move (destination_file, GLib.FileCopyFlags.OVERWRITE); } private void restore_database_file (GLib.File database_file) throws GLib.Error { var backup_path = @"$(database_file.get_path()).backup"; var backup_file = GLib.File.new_for_path (backup_path); backup_file.copy (database_file, GLib.FileCopyFlags.OVERWRITE); } private string build_database_path () { var directory_path = GLib.Path.build_filename (GLib.Environment.get_user_data_dir (), Config.PACKAGE_NAME); return GLib.Path.build_filename (directory_path, "database.sqlite"); } private string build_backup_path () { return @"$(build_database_path()).backup"; } private void delete_file_if_exists (string path) { var file = GLib.File.new_for_path (path); try { if (file.query_exists ()) { file.delete (); } } catch (GLib.Error error) { } } /** * Create database backup using SQLite's Backup API. * * This function internally uses queue_read to run the backup in the * GOM worker thread, then blocks the calling thread until completion. * * The backup is created as a temporary file (backup_path~) and only * moved to the final location after successful completion, ensuring * we never overwrite a good backup with a broken one. */ private void create_backup (string backup_path) { if (Ft.Database.adapter == null) { return; } Ft.Database.adapter.queue_read ( (adapter) => { var temp_backup_path = @"$(backup_path)~"; var success = false; unowned Sqlite.Database db = adapter.get_handle (); Sqlite.Database backup_db; delete_file_if_exists (temp_backup_path); if (Sqlite.Database.open (temp_backup_path, out backup_db) != Sqlite.OK) { GLib.warning ("Failed to open temporary backup database: %s", backup_db.errmsg ()); return; } Sqlite.Backup? backup = new Sqlite.Backup (backup_db, "main", db, "main"); if (backup == null) { GLib.warning ("Failed to initialize backup"); return; } // Perform backup in a single step var result = backup.step (-1); if (result == Sqlite.DONE) { try { var temp_backup_file = GLib.File.new_for_path (temp_backup_path); var backup_file = GLib.File.new_for_path (backup_path); temp_backup_file.move (backup_file, GLib.FileCopyFlags.OVERWRITE); success = true; } catch (GLib.Error error) { GLib.warning ("Failed to move temporary backup to final location: %s", error.message); } } else { GLib.warning ("Backup failed with error code: %d", result); } if (!success) { delete_file_if_exists (temp_backup_path); } }); } private bool should_create_backup (string backup_path) { var backup_file = GLib.File.new_for_path (backup_path); if (!backup_file.query_exists ()) { return true; } try { var info = backup_file.query_info (GLib.FileAttribute.TIME_MODIFIED, GLib.FileQueryInfoFlags.NONE); var modification_datetime = info.get_modification_date_time (); var now = new GLib.DateTime.now_local (); return modification_datetime == null || modification_datetime.get_year () != now.get_year () || modification_datetime.get_month () != now.get_month () || modification_datetime.get_day_of_month () != now.get_day_of_month (); } catch (GLib.Error error) { GLib.warning ("Failed to check last backup time: %s", error.message); return true; } } public void schedule_backup () { if (Ft.is_test ()) { return; } GLib.Idle.add (() => { var backup_path = build_backup_path (); if (should_create_backup (backup_path)) { create_backup (backup_path); } return GLib.Source.REMOVE; }, GLib.Priority.LOW); } private void open_repository (GLib.File? database_file, out Gom.Adapter? adapter, out Gom.Repository? repository) { adapter = new Gom.Adapter (); repository = null; try { if (database_file != null) { make_directory_with_parents (database_file.get_parent ()); adapter.open_sync (database_file.get_uri ()); } else { adapter.open_sync (":memory:"); } } catch (GLib.Error error) { GLib.critical ("Failed to open database '%s': %s", database_file?.get_uri (), error.message); // XXX: try recovery? return; } // Check database health after opening. // Restore from backup if the file is corrupt. if (database_file != null && !Ft.is_test () && !check_health (adapter)) { try { adapter.close_sync (); adapter = null; } catch (GLib.Error error) { GLib.warning ("Error closing corrupted database: %s", error.message); } // Move corrupted file aside try { rename_corrupted_file (database_file); } catch (GLib.Error error) { GLib.critical ("Could not rename corrupted database: %s", error.message); return; } // Try to restore from backup var has_backup = false; try { restore_database_file (database_file); has_backup = true; } catch (GLib.Error error) { } // Reopen (either with restored backup or create fresh database) try { adapter = new Gom.Adapter (); adapter.open_sync (database_file.get_uri ()); if (has_backup) { GLib.info ("Restored database from backup"); } else { GLib.info ("No backup available, created a fresh database"); } } catch (GLib.Error error) { GLib.critical ("Failed to open new database: %s", error.message); return; } } adapter.queue_write ( (_adapter) => { try { _adapter.execute_sql ("PRAGMA foreign_keys = ON;"); } catch (GLib.Error error) { GLib.warning ("Failed to enable 'foreign_keys': %s", error.message); } }); try { repository = new Gom.Repository (adapter); repository.migrate_sync (Ft.Database.VERSION, Ft.Database.migrate_repository); } catch (GLib.Error error) { GLib.error ("Failed to migrate database: %s", error.message); } } public void open () { GLib.File? database_file = null; if (Ft.Database.repository != null) { return; } if (!Ft.is_test ()) { var database_path = build_database_path (); database_file = GLib.File.new_for_path (database_path); var directory_file = database_file.get_parent (); if (directory_file != null && !directory_file.query_exists ()) { make_directory_with_parents (directory_file); } // Import database from the old app // XXX: let users migrate their data, but remove this at some point string old_database_path; if (Ft.is_flatpak ()) { old_database_path = GLib.Path.build_filename (GLib.Environment.get_home_dir (), ".local", "share", "gnome-pomodoro", "database.sqlite"); } else { old_database_path = GLib.Path.build_filename (GLib.Environment.get_user_data_dir (), "gnome-pomodoro", "database.sqlite"); } var old_database_file = GLib.File.new_for_path (old_database_path); if (!database_file.query_exists () && old_database_file.query_exists ()) { try { old_database_file.copy (database_file, GLib.FileCopyFlags.NONE, null, null); GLib.info ("Imported database from %s to %s", old_database_path, database_path); } catch (GLib.Error error) { GLib.warning ("Failed to import database: %s", error.message); } } } open_repository (database_file, out Ft.Database.adapter, out Ft.Database.repository); } private void close_repository (Gom.Repository repository) { var adapter = repository.adapter; try { adapter.close_sync (); } catch (GLib.Error error) { GLib.warning ("Error while closing database: %s", error.message); } } public void close () { if (Ft.Database.repository == null) { return; } close_repository (Ft.Database.repository); Ft.Database.repository = null; Ft.Database.adapter = null; } public string serialize_date (GLib.Date date) { return date.valid () ? Ft.DateUtils.format_date (date, DATE_FORMAT) : ""; } /** * Remove leading zeros from a string */ private inline string chug_zeros (string str) { var index = 0; while (str.@get (index) == '0') { index++; } return index > 0 ? str.substring (index) : str; } public GLib.Date parse_date (string date_string) { var parts = date_string.split ("-"); var date = GLib.Date (); if (parts.length == 3) { var year = uint.parse (chug_zeros (parts[0])); var month = uint.parse (chug_zeros (parts[1])); var day = uint.parse (chug_zeros (parts[2])); date.set_dmy ((GLib.DateDay) day, (GLib.DateMonth) month, (GLib.DateYear) year); } return date; } } focustimerhq-FocusTimer-8581be2/src/core/date-utils.vala000066400000000000000000000110241520625676500233140ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ [CCode (cprefix = "")] namespace Ft.DateUtils { public GLib.Date get_today () { var datetime = new GLib.DateTime.now_local (); var date = GLib.Date (); date.set_dmy ((GLib.DateDay) datetime.get_day_of_month (), (GLib.DateMonth) datetime.get_month (), (GLib.DateYear) datetime.get_year ()); return date; } public string format_date (GLib.Date date, string format) { // Avoid using `GLib.Date.strftime`, as it doesn't handle encodings // properly. var datetime = new GLib.DateTime.local ( date.get_year (), date.get_month (), date.get_day (), 0, 0, 0.0); return datetime.format (format); } private uint get_weekday_number_internal (GLib.DateWeekday weekday) { switch (weekday) { case GLib.DateWeekday.MONDAY: return 1U; case GLib.DateWeekday.TUESDAY: return 2U; case GLib.DateWeekday.WEDNESDAY: return 3U; case GLib.DateWeekday.THURSDAY: return 4U; case GLib.DateWeekday.FRIDAY: return 5U; case GLib.DateWeekday.SATURDAY: return 6U; case GLib.DateWeekday.SUNDAY: return 7U; default: return 0U; } } /** * Convert GLib.DateWeekday to integer * * The result is locale dependant. Starts from 1 - first day of a work week. */ public uint get_weekday_number (GLib.DateWeekday weekday) { var weekday_number = (int) get_weekday_number_internal (weekday); if (weekday_number != 0) { var first_day_of_week_number = (int) get_weekday_number_internal ( Ft.Locale.get_first_day_of_week ()); if (first_day_of_week_number == 0) { first_day_of_week_number = 7; // default to SUNDAY as the first day of week } var result = 1 + weekday_number - first_day_of_week_number; if (result < 1) { result += 7; } return (uint) result; } else { return 0U; } } public uint get_month_number (GLib.DateMonth month) { switch (month) { case GLib.DateMonth.JANUARY: return 1U; case GLib.DateMonth.FEBRUARY: return 2U; case GLib.DateMonth.MARCH: return 3U; case GLib.DateMonth.APRIL: return 4U; case GLib.DateMonth.MAY: return 5U; case GLib.DateMonth.JUNE: return 6U; case GLib.DateMonth.JULY: return 7U; case GLib.DateMonth.AUGUST: return 8U; case GLib.DateMonth.SEPTEMBER: return 9U; case GLib.DateMonth.OCTOBER: return 10U; case GLib.DateMonth.NOVEMBER: return 11U; case GLib.DateMonth.DECEMBER: return 12U; default: return 0U; } } public string get_month_name (GLib.DateMonth month) { return Locale.get_month_name (get_month_number (month)); } public GLib.Variant date_to_variant (GLib.Date date) { var day = new GLib.Variant.uint16 ((uint16) date.get_day ()); var month = new GLib.Variant.uint16 ((uint16) date.get_month ()); var year = new GLib.Variant.uint16 ((uint16) date.get_year ()); return new GLib.Variant.tuple ({ day, month, year }); } public GLib.Date date_from_variant (GLib.Variant variant) { var date = GLib.Date (); if (variant.get_type_string () == "(qqq)") { var day = (GLib.DateDay) variant.get_child_value (0).get_uint16 (); var month = (GLib.DateMonth) variant.get_child_value (1).get_uint16 (); var year = (GLib.DateYear) variant.get_child_value (2).get_uint16 (); if (day != GLib.DateDay.BAD_DAY && month != GLib.DateMonth.BAD_MONTH && year != GLib.DateYear.BAD_YEAR) { date.set_dmy (day, month, year); } } return date; } } focustimerhq-FocusTimer-8581be2/src/core/event-bus.vala000066400000000000000000000245541520625676500231650ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public delegate void EventCallback (Ft.Event event); public delegate void ConditionCallback (Ft.Context context); [SingleInstance] public class EventBus : GLib.Object { [Compact] private class EventWatch { public uint id; public string event_name; public Ft.Expression? condition; public Ft.EventCallback callback; ~EventWatch () { this.event_name = null; this.condition = null; this.callback = null; } public bool check_condition (Ft.Context context) { if (this.condition == null) { return true; } try { var result = this.condition.evaluate (context); return result != null ? result.to_boolean () : false; } catch (Ft.ExpressionError error) { GLib.warning ("Error while evaluating event condition: %s", error.message); return false; } } } [Compact] private class ConditionWatch { public uint id; public Ft.Expression condition; public Ft.ConditionCallback? enter_callback; public Ft.ConditionCallback? leave_callback; public bool active = false; ~ConditionWatch () { this.condition = null; this.enter_callback = null; this.leave_callback = null; } public void check_condition (Ft.Context context) { var active = this.active; try { var result = this.condition.evaluate (context); active = result != null ? result.to_boolean () : false; } catch (Ft.ExpressionError error) { GLib.warning ("Error while evaluating condition: %s", error.message); return; } if (this.active != active) { if (this.active && this.leave_callback != null) { this.leave_callback (context); } if (active && this.enter_callback != null) { this.enter_callback (context); } this.active = active; } } } private static uint next_watch_id = 1; private Ft.Context? last_context = null; private GLib.HashTable event_watches = null; private GLib.HashTable> event_watches_by_name = null; private GLib.HashTable condition_watches = null; private uint check_conditions_idle_id = 0; construct { this.event_watches = new GLib.HashTable ( GLib.direct_hash, GLib.direct_equal); this.event_watches_by_name = new GLib.HashTable> ( GLib.str_hash, GLib.str_equal); this.condition_watches = new GLib.HashTable ( GLib.direct_hash, GLib.direct_equal); } private void check_conditions (Ft.Context context) { if (this.check_conditions_idle_id != 0) { GLib.Source.remove (this.check_conditions_idle_id); this.check_conditions_idle_id = 0; } if (context == this.last_context) { return; } this.condition_watches.@foreach ( (id, watch) => { watch.check_condition (context); }); this.last_context = context; } private void schedule_check_conditions () { if (this.check_conditions_idle_id != 0) { return; } this.check_conditions_idle_id = GLib.Idle.add ( () => { this.check_conditions_idle_id = 0; this.check_conditions (new Ft.Context.build ()); return GLib.Source.REMOVE; }, GLib.Priority.DEFAULT ); GLib.Source.set_name_by_id (this.check_conditions_idle_id, "Ft.EventBus.check_conditions"); } public void push_event (Ft.Event event) { this.event (event); } public uint add_event_watch (string event_name, Ft.Expression? condition, owned Ft.EventCallback callback) { var watch_id = Ft.EventBus.next_watch_id; Ft.EventBus.next_watch_id++; var watch = new EventWatch (); watch.id = watch_id; watch.event_name = event_name; watch.condition = condition; watch.callback = (owned) callback; unowned var unowned_watch = watch; unowned var unowned_watches_array = this.event_watches_by_name.lookup (event_name); this.event_watches.insert (watch_id, (owned) watch); if (unowned_watches_array == null) { var watches_array = new GLib.Array (); watches_array.append_val (unowned_watch); this.event_watches_by_name.insert (event_name, watches_array); } else { unowned_watches_array.append_val (unowned_watch); } return watch_id; } public uint add_condition_watch (Ft.Expression condition, owned Ft.ConditionCallback? enter_callback, owned Ft.ConditionCallback? leave_callback) { var watch_id = Ft.EventBus.next_watch_id; Ft.EventBus.next_watch_id++; var watch = new ConditionWatch (); watch.id = watch_id; watch.condition = condition; watch.enter_callback = (owned) enter_callback; watch.leave_callback = (owned) leave_callback; this.condition_watches.insert (watch_id, (owned) watch); this.schedule_check_conditions (); return watch_id; } public void remove_event_watch (uint watch_id) { unowned var watch = this.event_watches.lookup (watch_id); if (watch == null) { GLib.warning ("Unable to remove event watch %u.", watch_id); return; } unowned var watches_array = this.event_watches_by_name.lookup (watch.event_name); if (watches_array != null) { for (var index = 0U; index < watches_array.length; index++) { unowned var item = watches_array.index (index); if (item == watch) { watches_array.remove_index (index); break; } } } this.event_watches.remove (watch_id); } public void remove_condition_watch (uint watch_id) { unowned var watch = this.condition_watches.lookup (watch_id); if (watch == null) { GLib.warning ("Unable to remove condition watch %u.", watch_id); return; } if (watch.active) { if (watch.leave_callback != null) { watch.leave_callback (new Ft.Context.build ()); } watch.active = false; } this.condition_watches.remove (watch_id); } public void destroy () { if (this.check_conditions_idle_id != 0) { GLib.Source.remove (this.check_conditions_idle_id); this.check_conditions_idle_id = 0; } var context = new Ft.Context.build (); this.condition_watches.@foreach ( (id, watch) => { if (watch.active) { if (watch.leave_callback != null) { watch.leave_callback (context); } watch.active = false; } }); } public signal void event (Ft.Event event) { unowned var watches_array = this.event_watches_by_name.lookup (event.spec.name); if (watches_array != null) { for (var index = 0U; index < watches_array.length; index++) { unowned var watch = watches_array.index (index); if (watch.check_condition (event.context)) { watch.callback (event); } } } // TODO: Some events like "reschedule" are followed by an another, so there's little reason to check // conditions again. Perhaps such events could have a flag. if (event.spec.name != "reschedule") { this.check_conditions (event.context); } } public override void dispose () { this.destroy (); this.last_context = null; if (this.event_watches_by_name != null) { this.event_watches_by_name.remove_all (); this.event_watches_by_name = null; } if (this.event_watches != null) { this.event_watches.remove_all (); this.event_watches = null; } if (this.condition_watches != null) { this.condition_watches.remove_all (); this.condition_watches = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/event-producer.vala000066400000000000000000000422751520625676500242170ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [CCode (has_target = false)] public delegate bool TriggerFunc (); [CCode (has_target = false)] public delegate bool TimerStateChangedTriggerFunc ( Ft.TimerState current_state, Ft.TimerState previous_state); [CCode (has_target = false)] public delegate bool SessionManagerConfirmAdvancementTriggerFunc ( Ft.TimeBlock next_time_block, Ft.TimeBlock previous_time_block); [CCode (has_target = false)] public delegate bool SessionManagerAdvancedTriggerFunc ( Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block); [CCode (has_target = false)] public delegate bool SessionManagerNotifyCurrentStateTriggerFunc ( Ft.State current_state, Ft.State previous_state); [CCode (has_target = false)] public delegate bool SessionManagerSessionRescheduledTriggerFunc ( Ft.Session session); [CCode (has_target = false)] public delegate bool SessionManagerSessionExpiredTriggerFunc ( Ft.Session session); public enum TriggerHook { NONE, TIMER_STATE_CHANGED, SESSION_MANAGER_CONFIRM_ADVANCEMENT, SESSION_MANAGER_ADVANCED, SESSION_MANAGER_NOTIFY_CURRENT_STATE, SESSION_MANAGER_SESSION_RESCHEDULED, SESSION_MANAGER_SESSION_EXPIRED } public struct Trigger { public unowned Ft.EventSpec event_spec; public Ft.TriggerHook hook; public Ft.TriggerFunc func; } /** * Gathers events from various sources and pushes them onto a bus. It's not 1:1 mapping; events are supposed to be * more intuitive for the user. Some events are filtered to not trigger them unnecessarily, and some are delayed * to collect fuller context. */ [SingleInstance] public class EventProducer : GLib.Object { public Ft.SessionManager session_manager { get { return this._session_manager; } construct { this._session_manager = value; this._session_manager.confirm_advancement.connect (this.on_session_manager_confirm_advancement); this._session_manager.advanced.connect (this.on_session_manager_advanced); this._session_manager.notify["current-state"].connect (this.on_session_manager_notify_current_state); this._session_manager.session_rescheduled.connect (this.on_session_manager_session_rescheduled); this._session_manager.session_expired.connect (this.on_session_manager_session_expired); this.previous_state = this._session_manager.current_state; } } public Ft.Timer timer { get { return this._timer; } construct { this._timer = value; this._timer.state_changed.connect (this.on_timer_state_changed); } } public Ft.EventBus bus { get { return this._bus; } construct { this._bus = value; } } private Ft.SessionManager? _session_manager = null; private Ft.Timer? _timer = null; private Ft.EventBus? _bus = null; private Ft.EventSpec[] event_specs = null; private GLib.HashTable event_spec_by_name = null; private Ft.State previous_state; private GLib.Queue queue = null; private uint idle_id = 0; private int64 event_source_timestamp = Ft.Timestamp.UNDEFINED; private int64 last_session_rescheduled_time = Ft.Timestamp.UNDEFINED; private bool destroying = false; private Ft.Trigger[] timer_state_change_triggers; private Ft.Trigger[] session_manager_confirm_advancement_triggers; private Ft.Trigger[] session_manager_advanced_triggers; private Ft.Trigger[] session_manager_notify_current_state_triggers; private Ft.Trigger[] session_manager_session_rescheduled_triggers; private Ft.Trigger[] session_manager_session_expired_triggers; construct { this.event_specs = new Ft.EventSpec[0]; this.event_spec_by_name = new GLib.HashTable (GLib.str_hash, GLib.str_equal); this.queue = new GLib.Queue (); this.timer_state_change_triggers = new Ft.Trigger[0]; this.session_manager_confirm_advancement_triggers = new Ft.Trigger[0]; this.session_manager_advanced_triggers = new Ft.Trigger[0]; this.session_manager_notify_current_state_triggers = new Ft.Trigger[0]; this.session_manager_session_rescheduled_triggers = new Ft.Trigger[0]; this.session_manager_session_expired_triggers = new Ft.Trigger[0]; Ft.initialize_events (this); } public EventProducer () { var bus = new Ft.EventBus (); GLib.Object ( session_manager: Ft.SessionManager.get_default (), timer: Ft.Timer.get_default (), bus: bus ); } public EventProducer.with_session_manager (Ft.SessionManager session_manager) { var bus = new Ft.EventBus (); GLib.Object ( session_manager: session_manager, timer: session_manager.timer, bus: bus ); } public void install_event (Ft.EventSpec event_spec) { unowned var unowned_event_spec = event_spec; foreach (var trigger in event_spec.triggers) { switch (trigger.hook) { case Ft.TriggerHook.TIMER_STATE_CHANGED: this.timer_state_change_triggers += trigger; break; case Ft.TriggerHook.SESSION_MANAGER_CONFIRM_ADVANCEMENT: this.session_manager_confirm_advancement_triggers += trigger; break; case Ft.TriggerHook.SESSION_MANAGER_ADVANCED: this.session_manager_advanced_triggers += trigger; break; case Ft.TriggerHook.SESSION_MANAGER_NOTIFY_CURRENT_STATE: this.session_manager_notify_current_state_triggers += trigger; break; case Ft.TriggerHook.SESSION_MANAGER_SESSION_RESCHEDULED: this.session_manager_session_rescheduled_triggers += trigger; break; case Ft.TriggerHook.SESSION_MANAGER_SESSION_EXPIRED: this.session_manager_session_expired_triggers += trigger; break; default: assert_not_reached (); } } if (event_spec_by_name.insert (event_spec.name, unowned_event_spec)) { this.event_specs += event_spec; } else { GLib.error ("Unable to install event '%s'", event_spec.name); } } public unowned Ft.EventSpec? find_event (string event_name) { return this.event_spec_by_name.lookup (event_name); } public (unowned Ft.EventSpec)[] list_events () { return this.event_specs; } private void trigger_queued_events (Ft.Context context) { Ft.EventSpec event_spec; if (this.idle_id != 0) { GLib.Source.remove (this.idle_id); this.idle_id = 0; } while ((event_spec = this.queue.pop_head ()) != null) { this.event (new Ft.Event (event_spec, context)); } } private void trigger_event (Ft.EventSpec event_spec, int64 timestamp) { if (this.destroying) { return; } var context = new Ft.Context.build (timestamp); this.trigger_queued_events (context); this.event (new Ft.Event (event_spec, context)); } private bool on_idle () { var timestamp = this.event_source_timestamp; if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = int64.max (this._timer.get_last_state_changed_time (), this._timer.get_last_tick_time ()); } this.idle_id = 0; this.event_source_timestamp = Ft.Timestamp.UNDEFINED; this.trigger_queued_events (new Ft.Context.build (timestamp)); return GLib.Source.REMOVE; } private void queue_event (Ft.EventSpec event_spec) { if (this.destroying) { return; } // TODO: preserve event current timestamp, for the context this.queue.push_tail (event_spec); this.event_source_timestamp = Ft.Context.get_event_source_timestamp (); if (this.idle_id == 0) { this.idle_id = GLib.Idle.add (this.on_idle, GLib.Priority.DEFAULT); GLib.Source.set_name_by_id (this.idle_id, "Ft.EventProducer.trigger_queued_events"); } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { var timestamp = this._timer.get_last_state_changed_time (); foreach (var trigger in this.timer_state_change_triggers) { var trigger_func = (Ft.TimerStateChangedTriggerFunc) trigger.func; if (trigger_func (current_state, previous_state)) { this.trigger_event (trigger.event_spec, timestamp); } } } private void on_session_manager_confirm_advancement (Ft.TimeBlock current_time_block, Ft.TimeBlock next_time_block) { var timestamp = current_time_block.end_time; foreach (var trigger in this.session_manager_confirm_advancement_triggers) { var trigger_func = (Ft.SessionManagerConfirmAdvancementTriggerFunc) trigger.func; if (trigger_func (current_time_block, next_time_block)) { this.trigger_event (trigger.event_spec, timestamp); } } } private void on_session_manager_advanced (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block) { var timestamp = this._timer.get_last_state_changed_time (); if (previous_time_block != null) { timestamp = previous_time_block.end_time; } if (current_time_block != null) { timestamp = current_time_block.start_time; } foreach (var trigger in this.session_manager_advanced_triggers) { var trigger_func = (Ft.SessionManagerAdvancedTriggerFunc) trigger.func; if (trigger_func (current_session, current_time_block, previous_session, previous_time_block)) { this.trigger_event (trigger.event_spec, timestamp); } } } private void on_session_manager_notify_current_state (GLib.Object object, GLib.ParamSpec pspec) { var current_state = this._session_manager.current_state; var previous_state = this.previous_state; if (current_state == previous_state) { return; } this.previous_state = current_state; foreach (var trigger in this.session_manager_notify_current_state_triggers) { var trigger_func = (Ft.SessionManagerNotifyCurrentStateTriggerFunc) trigger.func; if (trigger_func (current_state, previous_state)) { this.queue_event (trigger.event_spec); } } } private void on_session_manager_session_rescheduled (Ft.Session session, int64 timestamp) { // Workaround for redundant signals. // FIXME: It should be fixed in SessionManager - it calls rescheduling too often at times if (timestamp == this.last_session_rescheduled_time) { GLib.debug ("Detected duplicate 'session-rescheduled' event. Dropping..."); return; } this.last_session_rescheduled_time = timestamp; foreach (var trigger in this.session_manager_session_rescheduled_triggers) { var trigger_func = (Ft.SessionManagerSessionRescheduledTriggerFunc) trigger.func; if (trigger_func (session)) { // Reschedule is typically done as first thing before any action. // To capture timer context we need to to delay collecting the context. this.queue_event (trigger.event_spec); } } } private void on_session_manager_session_expired (Ft.Session session, int64 timestamp) { foreach (var trigger in this.session_manager_session_expired_triggers) { var trigger_func = (Ft.SessionManagerSessionExpiredTriggerFunc) trigger.func; if (trigger_func (session)) { this.trigger_event (trigger.event_spec, timestamp); } } } public void flush () { this.trigger_queued_events (new Ft.Context.build ()); } public void destroy () { this.destroying = true; this.trigger_queued_events (new Ft.Context.build ()); } [Signal (run = "first")] public signal void event (Ft.Event event) { this._bus.push_event (event); } public override void dispose () { if (this.idle_id != 0) { GLib.Source.remove (this.idle_id); this.idle_id = 0; } if (this.event_spec_by_name != null) { this.event_spec_by_name.remove_all (); } if (this._timer != null) { this._timer.state_changed.disconnect (this.on_timer_state_changed); this._timer = null; } if (this._session_manager != null) { this._session_manager.confirm_advancement.disconnect (this.on_session_manager_confirm_advancement); this._session_manager.advanced.disconnect (this.on_session_manager_advanced); this._session_manager.notify["current-state"].disconnect (this.on_session_manager_notify_current_state); this._session_manager.session_rescheduled.disconnect (this.on_session_manager_session_rescheduled); this._session_manager.session_expired.disconnect (this.on_session_manager_session_expired); this._session_manager = null; } this._bus = null; this.event_specs = null; this.event_spec_by_name = null; this.queue = null; this.event_source_timestamp = Ft.Timestamp.UNDEFINED; this.timer_state_change_triggers = null; this.session_manager_confirm_advancement_triggers = null; this.session_manager_advanced_triggers = null; this.session_manager_notify_current_state_triggers = null; this.session_manager_session_rescheduled_triggers = null; this.session_manager_session_expired_triggers = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/event.vala000066400000000000000000000345331520625676500223740ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { private bool trigger_start_event (Ft.TimerState current_state, Ft.TimerState previous_state) { if (Ft.Context.get_event_source () != "timer.start") { return false; } return current_state.user_data != null && previous_state.user_data == null; } private bool trigger_stop_event (Ft.TimerState current_state, Ft.TimerState previous_state) { if (Ft.Context.get_event_source () != "timer.reset") { return false; } return current_state.user_data == null && previous_state.user_data != null; } private bool trigger_pause_event (Ft.TimerState current_state, Ft.TimerState previous_state) { if (Ft.Context.get_event_source () != "timer.pause") { return false; } return current_state.user_data == previous_state.user_data && current_state.is_paused () && !previous_state.is_paused (); } private bool trigger_resume_event (Ft.TimerState current_state, Ft.TimerState previous_state) { if (Ft.Context.get_event_source () != "timer.resume") { return false; } return current_state.user_data == previous_state.user_data && !current_state.is_paused () && previous_state.is_paused (); } private bool trigger_rewind_event (Ft.TimerState current_state, Ft.TimerState previous_state) { if (Ft.Context.get_event_source () != "timer.rewind") { return false; } return current_state.user_data == previous_state.user_data && current_state.paused_time == previous_state.paused_time && current_state.offset != previous_state.offset; } private bool trigger_skip_event (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block) { if (Ft.Context.get_event_source () != "session-manager.advance") { return false; } if (current_time_block == null || previous_time_block == null) { return false; } if (current_time_block.state != Ft.State.POMODORO && previous_time_block.state != Ft.State.POMODORO) { return false; } if (current_time_block.state.is_break () == previous_time_block.state.is_break ()) { return false; } return current_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS && previous_time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED; } private bool trigger_reset_event (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block) { if (Ft.Context.get_event_source () != "session-manager.reset") { return false; } if (current_session == previous_session || previous_session == null) { return false; } if (current_session == null) { return true; } return !previous_session.is_completed (); } private bool trigger_finish_event (Ft.TimerState current_state, Ft.TimerState previous_state) { return current_state.is_finished () && !previous_state.is_finished (); } private bool trigger_confirm_advancement_event (Ft.TimeBlock next_time_block, Ft.TimeBlock previous_time_block) { return true; } private bool trigger_advance_event (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block) { // Internally `SessionManager.advanced` includes a stopped state, as it follows the call to `advance_*` // methods. For user this behaviour may not be obvious. The second thing is that it would intersect with // `start` and `stop` events. It makes more sense to have `start`, `stop` and `advance` events // complementary. return current_time_block != null && previous_time_block != null; } private bool trigger_change_event (Ft.TimerState current_state, Ft.TimerState previous_state) { // If timer finishes no true change has been done - the timer finished according to plan. if (current_state.is_finished () && current_state.user_data == previous_state.user_data) { return false; } return !current_state.equals (previous_state); } private bool trigger_state_change_event (Ft.State current_state, Ft.State previous_state) { return true; } private bool trigger_reschedule_event (Ft.Session session) { return true; } private bool trigger_expire_event (Ft.Session session) { return true; } public enum EventCategory { OTHER, ACTIONS, COUNTDOWN, SESSION; public string get_label () { switch (this) { case ACTIONS: return _("Actions"); case COUNTDOWN: return _("Countdown"); case SESSION: return _("Session"); case OTHER: return _("Other"); default: assert_not_reached (); } } public static void @foreach (GLib.Func func) { func (ACTIONS); func (COUNTDOWN); func (SESSION); func (OTHER); } } public class EventSpec { public string name; public string display_name; public string description; public Ft.EventCategory category; internal Ft.Trigger[] triggers; public EventSpec (string name, string display_name, string description, Ft.EventCategory category = Ft.EventCategory.OTHER) { this.name = name; this.display_name = display_name; this.description = description; this.category = category; this.triggers = new Ft.Trigger[0]; } public void add_trigger (Ft.TriggerHook trigger_hook, Ft.TriggerFunc trigger_func) { unowned var self = this; this.triggers += Ft.Trigger () { event_spec = self, hook = trigger_hook, func = trigger_func }; } } [Compact] public class Event { public Ft.EventSpec spec; public Ft.Context context; public Event (Ft.EventSpec spec, Ft.Context context) { this.spec = spec; this.context = context; } ~Event () { this.spec = null; this.context = null; } } internal void initialize_events (Ft.EventProducer producer) { Ft.EventSpec event_spec; // Actions event_spec = new Ft.EventSpec ("start", _("Start"), _("Started the timer."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_start_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("stop", _("Stop"), _("Stopped the timer manually."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_stop_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("pause", _("Pause"), _("The countdown has been manually paused. Not triggered when locking the screen or when suspending the system."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_pause_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("resume", _("Resume"), _("The countdown has been manually resumed."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_resume_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("skip", _("Skip"), _("Jumped to a next time-block before the countdown has finished."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_ADVANCED, (Ft.TriggerFunc) trigger_skip_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("rewind", _("Rewind"), _("Rewind action has been used. It adds a pause in the past."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_rewind_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("reset", _("Reset"), _("Manually cleared the session."), Ft.EventCategory.ACTIONS); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_ADVANCED, (Ft.TriggerFunc) trigger_reset_event); producer.install_event (event_spec); // Countdown event_spec = new Ft.EventSpec ("finish", _("Finished"), _("The countdown has finished. If waiting for confirmation, the duration of the time-block still may be altered."), Ft.EventCategory.COUNTDOWN); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_finish_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("change", _("Changed"), _("Triggered on any change related to the countdown."), Ft.EventCategory.COUNTDOWN); event_spec.add_trigger (Ft.TriggerHook.TIMER_STATE_CHANGED, (Ft.TriggerFunc) trigger_change_event); producer.install_event (event_spec); // Session event_spec = new Ft.EventSpec ("confirm-advancement", _("Confirm Advancement"), _("A manual confirmation is required to start next time-block."), Ft.EventCategory.SESSION); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_CONFIRM_ADVANCEMENT, (Ft.TriggerFunc) trigger_confirm_advancement_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("advance", _("Advanced"), _("Transitioned or skipped to a next time-block."), Ft.EventCategory.SESSION); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_ADVANCED, (Ft.TriggerFunc) trigger_advance_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("state-change", _("State Changed"), _("Transitioned to a next time-block or when a break gets relabelled."), Ft.EventCategory.SESSION); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_NOTIFY_CURRENT_STATE, (Ft.TriggerFunc) trigger_state_change_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("reschedule", _("Rescheduled"), // translators: Change of plan _("Triggered when scheduled time-blocks have changed."), Ft.EventCategory.SESSION); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_SESSION_RESCHEDULED, (Ft.TriggerFunc) trigger_reschedule_event); producer.install_event (event_spec); event_spec = new Ft.EventSpec ("expire", _("Expired"), _("Triggered when session is about to be reset due to inactivity."), Ft.EventCategory.SESSION); event_spec.add_trigger (Ft.TriggerHook.SESSION_MANAGER_SESSION_EXPIRED, (Ft.TriggerFunc) trigger_expire_event); producer.install_event (event_spec); } } focustimerhq-FocusTimer-8581be2/src/core/expression-parser.vala000066400000000000000000000602241520625676500247400ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public errordomain ExpressionParserError { SYNTAX_ERROR, UNKNOWN_IDENTIFIER, } private enum TokenType { INVALID, STRING_LITERAL, NUMERIC_LITERAL, IDENTIFIER, OPERATOR, PARENTHESIS, } [Compact] private class Token { public TokenType type; public int span_start; public int span_end; public string text; public weak Token? prev; public weak Token? next; public weak Token? parent; public weak Token? children; // first child public uint precedence = 0U; public weak Token? matching_token; ~Token () { this.text = null; this.prev = null; this.next = null; this.parent = null; this.children = null; this.matching_token = null; } public uint n_children () { unowned var child = this.children; var n_children = 0U; while (child != null) { child = child.next; n_children++; } return n_children; } public unowned Token get_root () { unowned var root = this; while (root.parent != null) { root = root.parent; } return root; } public void append (Token token) requires (token.parent == null) { unowned var last_token = this; while (last_token.next != null) { last_token = last_token.next; } last_token.next = token; token.prev = last_token; token.parent = this.parent; } public inline void append_child (Token token) requires (token.parent == null) { if (this.children == null) { token.parent = this; this.children = token; } else { this.children.append (token); } } /** * Insert given token as a new parent. */ public void insert_parent (Token token) requires (token.parent == null && token.children == null) requires (token.prev == null && token.next == null) { if (this.parent != null && this.parent.children == this) { this.parent.children = token; } if (this.prev != null) { this.prev.next = token; } token.parent = this.parent; token.prev = this.prev; token.children = this; this.parent = token; this.prev = null; } } [Compact] internal class ExpressionParser { private const uint BASE_OPERATOR_PRECEDENCE = 1000U; public GLib.HashTable operators; public ExpressionParser () { this.operators = new GLib.HashTable ( GLib.str_hash, GLib.str_equal); Ft.Operator.@foreach ( (operator) => { operators.insert (operator.to_string (), operator); }); } ~ExpressionParser () { this.operators = null; } private static string format_error_context (string text, int index, uint limit = 10) { int span_start = index; int span_end = index; int n = 0; unichar chr; while (n < limit && text.get_next_char (ref span_end, out chr)) { n++; } return "'%s...' at position %d".printf ( text.substring (span_start, span_end - span_start), span_start); } private Token tokenize_string_literal (string text, ref int index) throws Ft.ExpressionParserError { var token = new Token () { type = TokenType.STRING_LITERAL, span_start = index, }; var is_escaped = false; var string_builder = new GLib.StringBuilder (); unichar chr; index++; // skip first quote while (text.get_next_char (ref index, out chr)) { if (chr == '\\') { is_escaped = !is_escaped; continue; } if (is_escaped) { switch (chr) { case 'n': string_builder.append ("\n"); break; case 'r': string_builder.append ("\r"); break; case 't': string_builder.append ("\t"); break; case 'f': string_builder.append ("\f"); break; case 'b': string_builder.append ("\b"); break; default: string_builder.append_unichar (chr); break; } is_escaped = false; continue; } if (chr == '"') { token.span_end = index; token.text = string_builder.str.dup (); // TODO: is dup() necessary? return token; } string_builder.append_unichar (chr); } throw new Ft.ExpressionParserError.SYNTAX_ERROR ("Unquoted string at %d", index); } private Token tokenize_identifier (string text, ref int index) { var token = new Token () { type = TokenType.IDENTIFIER, span_start = index, span_end = index + 1, }; unichar chr; index++; // skip first char while (text.get_next_char (ref index, out chr)) { if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z') || (chr >= '0' && chr <= '9')) { token.span_end = index; continue; } else { index = token.span_end; break; } } token.text = text.substring ((long) token.span_start, (long) (token.span_end - token.span_start)); return token; } private Token tokenize_operator (string text, ref int index) throws Ft.ExpressionParserError { var token = new Token () { type = TokenType.OPERATOR, span_start = index, span_end = index + 1, }; unichar chr; index++; // skip first character while (text.get_next_char (ref index, out chr)) { if (chr == '|' || chr == '&' || chr == '=' || chr == '!' || chr == '>' || chr == '<') { token.span_end = index; } else { index = token.span_end; break; } } token.text = text.substring (token.span_start, token.span_end - token.span_start); if (!this.operators.contains (token.text)) { throw new Ft.ExpressionParserError.SYNTAX_ERROR ( "Invalid operator '%s' at position %d", token.text, index); } return token; } private Token? tokenize_numeric_literal (string text, ref int index) { var token = new Token () { type = TokenType.NUMERIC_LITERAL, span_start = index, span_end = index + 1, }; unichar chr; index++; // skip first char while (text.get_next_char (ref index, out chr)) { if (chr >= '0' && chr <= '9') { token.span_end = index; continue; } else { index = token.span_end; break; } } token.text = text.substring ((long) token.span_start, (long) (token.span_end - token.span_start)); return token; } private GLib.Array tokenize (string text) throws Ft.ExpressionParserError { var tokens = new GLib.Array.sized (false, true, 8U); unichar chr; int chr_span_start = 0; int chr_span_end = 0; while (text.get_next_char (ref chr_span_end, out chr)) { switch (chr) { case ' ': break; case '(': case ')': tokens.append_val ( new Token () { type = TokenType.PARENTHESIS, span_start = chr_span_start, span_end = chr_span_end, text = chr.to_string (), }); break; case '"': chr_span_end = chr_span_start; tokens.append_val (this.tokenize_string_literal (text, ref chr_span_end)); break; case '|': case '&': case '=': case '!': case '>': case '<': chr_span_end = chr_span_start; tokens.append_val (this.tokenize_operator (text, ref chr_span_end)); break; default: if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')) { chr_span_end = chr_span_start; tokens.append_val (this.tokenize_identifier (text, ref chr_span_end)); break; } if ((chr >= '0' && chr <= '9') || chr == '-') { chr_span_end = chr_span_start; tokens.append_val ( this.tokenize_numeric_literal (text, ref chr_span_end)); break; } if (chr.isspace ()) { break; } throw new Ft.ExpressionParserError.SYNTAX_ERROR ( "Unexpected expression %s", format_error_context (text, chr_span_start)); } chr_span_start = chr_span_end; } return tokens; } private inline unowned Token? find_open_parenthesis (Token? token) { unowned var node = token; while (node != null) { if (node.type == Ft.TokenType.PARENTHESIS && node.matching_token == null) { return node; } node = node.parent; } return null; } /** * Validate roughly if syntax makes sense. */ private inline bool is_valid_token (Token token, Token? previous_token) { if (previous_token == null) { return token.type != TokenType.OPERATOR; } switch (previous_token.type) { case TokenType.OPERATOR: return token.type != TokenType.OPERATOR; case TokenType.PARENTHESIS: return true; // the type of parenthesis is validated later case TokenType.STRING_LITERAL: case TokenType.NUMERIC_LITERAL: case TokenType.IDENTIFIER: return token.type == OPERATOR || token.type == PARENTHESIS; default: assert_not_reached (); } } private inline bool is_valid_expression (Token? last_token) { if (last_token == null) { return true; } if (last_token.type == TokenType.OPERATOR) { return false; } if (this.find_open_parenthesis (last_token) != null) { return false; } return true; } private inline uint determine_precedence (Token token, Token? previous_token) { switch (token.type) { case TokenType.OPERATOR: var operator = this.operators.lookup (token.text); return BASE_OPERATOR_PRECEDENCE + operator.get_precedence (); case TokenType.PARENTHESIS: // We only keep the starting token in the final tree. The closing parenthesis // is kept as `token.matching_token` to indicate whether parenthesis is still // open while building the token tree. return previous_token != null && previous_token.type == Ft.TokenType.PARENTHESIS ? previous_token.precedence - 1U : BASE_OPERATOR_PRECEDENCE; default: return 0U; } } private inline unowned Token? link_tokens (Token token, Token? previous_token) { if (token.type == Ft.TokenType.OPERATOR) { // If the same operator is repeated, ignore it and use the first token. // Find closest operator. unowned var reference_token = previous_token; while (reference_token.parent != null && reference_token.type != Ft.TokenType.OPERATOR) { reference_token = reference_token.parent; } if (reference_token.type == Ft.TokenType.OPERATOR && reference_token.text == token.text) { return reference_token; // use first operator token for an operation } } if (previous_token != null) { // Find closest ancestor with higher precedence. unowned var reference_token = previous_token; if (token.precedence > 0 && reference_token.parent != null && reference_token.parent.precedence > token.precedence) // TODO: we only look at direct parent, use a loop to find `reference_token`? { reference_token = reference_token.parent; reference_token.insert_parent (token); assert (token.precedence < reference_token.precedence); } else if (token.precedence > reference_token.precedence) { reference_token.insert_parent (token); } else if (token.precedence < reference_token.precedence) { reference_token.append_child (token); } else { reference_token.append (token); } } return token; } /** * Convert array of tokens into an Abstract Syntax Tree (AST). * * The AST represents the hierarchical structure of the expression. For example * `A || B && C` we want to build a tree: * * || * / \ * A && * / \ * B C */ public unowned Token? build_token_tree (GLib.Array tokens) throws Ft.ExpressionParserError { unowned Token? token = null; unowned Token? previous_token = null; for (var index = 0U; index < tokens.length; index++) { token = tokens.index (index); // Validate token if (!this.is_valid_token (token, previous_token)) { throw new Ft.ExpressionParserError.SYNTAX_ERROR ( "Unexpected token '%s' at position %d", token.text, token.span_start); } if (token.type == TokenType.PARENTHESIS && token.text == ")") { unowned var reference_token = this.find_open_parenthesis (previous_token); if (reference_token == null) { throw new Ft.ExpressionParserError.SYNTAX_ERROR ("Unmatched parenthesis"); } reference_token.matching_token = token; previous_token = token = reference_token; continue; } // Determine precedence and associativity token.precedence = this.determine_precedence (token, previous_token); // Link tokens and form a tree token = this.link_tokens (token, previous_token); previous_token = token; } if (!this.is_valid_expression (token)) { throw new Ft.ExpressionParserError.SYNTAX_ERROR ( "Unexpected end of expression"); } return token?.get_root (); } /** * We serialize timestamps and enums to `string`, for the result expression to * look more like JSON. But this has cost - we now have to deduce original types from * the strings during parsing. */ private inline Ft.Value cast_string_literal (string text) { switch (text) { case "stopped": return new Ft.StateValue (Ft.State.STOPPED); case "pomodoro": return new Ft.StateValue (Ft.State.POMODORO); case "break": return new Ft.StateValue (Ft.State.BREAK); case "short-break": return new Ft.StateValue (Ft.State.SHORT_BREAK); case "long-break": return new Ft.StateValue (Ft.State.LONG_BREAK); case "scheduled": return new Ft.StatusValue (Ft.TimeBlockStatus.SCHEDULED); case "in-progress": return new Ft.StatusValue (Ft.TimeBlockStatus.IN_PROGRESS); case "completed": return new Ft.StatusValue (Ft.TimeBlockStatus.COMPLETED); case "uncompleted": return new Ft.StatusValue (Ft.TimeBlockStatus.UNCOMPLETED); default: break; } var timestamp = Ft.Timestamp.from_iso8601 (text); if (timestamp != Ft.Timestamp.UNDEFINED) { return new Ft.TimestampValue (timestamp); } return new Ft.StringValue (text); } private inline Ft.Value cast_numeric_literal (string text) { // XXX: handle overflow errors when parsing int? return new Ft.IntervalValue (int64.parse (text)); } private inline Ft.Expression? interpret_identifier (Token token) throws Ft.ExpressionParserError { assert (token.n_children () == 0); if (token.text == "true") { return new Ft.Constant (new Ft.BooleanValue (true)); } if (token.text == "false") { return new Ft.Constant (new Ft.BooleanValue (false)); } var name = Ft.from_camel_case (token.text); if (Ft.find_variable (name) == null) { throw new Ft.ExpressionParserError.UNKNOWN_IDENTIFIER ( "Unknown identifier '%s' at %d", token.text, token.span_start); } return new Ft.Variable (name); } private inline Ft.Expression? interpret_operator (Token token) throws Ft.ExpressionParserError { var operator = this.operators.lookup (token.text); var n_children = token.n_children (); unowned var child = token.children; switch (operator.get_category ()) { case Ft.OperatorCategory.LOGICAL: assert (n_children >= 2); var arguments = new Ft.Expression[n_children]; for (var index = 0; index < n_children; index++) { arguments[index] = this.interpret (child); child = child.next; } return new Ft.Operation.with_argv (operator, arguments); case Ft.OperatorCategory.COMPARISON: assert (n_children == 2); var argument_lhs = this.interpret (child); var argument_rhs = this.interpret (child.next); return new Ft.Comparison (argument_lhs, operator, argument_rhs); default: assert_not_reached (); } } /** * Convert token tree (aka AST) into our expression. */ private Ft.Expression? interpret (Token? token) throws Ft.ExpressionParserError { if (token == null) { return null; } switch (token.type) { case TokenType.STRING_LITERAL: assert (token.n_children () == 0); return new Ft.Constant (this.cast_string_literal (token.text)); case TokenType.NUMERIC_LITERAL: assert (token.n_children () == 0); return new Ft.Constant (this.cast_numeric_literal (token.text)); case TokenType.IDENTIFIER: assert (token.n_children () == 0); return this.interpret_identifier (token); case TokenType.OPERATOR: return this.interpret_operator (token); case TokenType.PARENTHESIS: return this.interpret (token.children); default: assert_not_reached (); } } public Ft.Expression? parse (string text) throws Ft.ExpressionParserError { var tokens = this.tokenize (text); unowned var root = this.build_token_tree (tokens); return this.interpret (root); } } } focustimerhq-FocusTimer-8581be2/src/core/expression.vala000066400000000000000000000722011520625676500234440ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { private inline string quote_string (string text) { // TODO: escape quotes return @"\"$(text)\""; } public errordomain ExpressionError { EMPTY, VALUE, TYPE, INVALID } public enum OperatorCategory { LOGICAL, COMPARISON } /** * Operator takes two inputs and produces one output. * * By this definition a "NOT" operation would not be an operator, but a function. */ public enum Operator { INVALID, // Logical AND, OR, // Comparison EQ, LT, LTE, GT, GTE, NOT_EQ; public Ft.OperatorCategory get_category () { switch (this) { case AND: case OR: return Ft.OperatorCategory.LOGICAL; case EQ: case LT: case LTE: case GT: case GTE: case NOT_EQ: return Ft.OperatorCategory.COMPARISON; default: assert_not_reached (); } } internal uint get_precedence () { switch (this) { case OR: return 1; case AND: return 2; case EQ: case LT: case LTE: case GT: case GTE: case NOT_EQ: return 3; default: assert_not_reached (); } } public static void @foreach (GLib.Func func) { Ft.Operator[] operators = { AND, OR, EQ, LT, LTE, GT, GTE, NOT_EQ, }; foreach (var operator in operators) { func (operator); } } private static inline Ft.Value apply_and (Ft.Value value_1, Ft.Value value_2) { return new Ft.BooleanValue (value_1.to_boolean () && value_2.to_boolean ()); } private static inline Ft.Value apply_or (Ft.Value value_1, Ft.Value value_2) { return new Ft.BooleanValue (value_1.to_boolean () || value_2.to_boolean ()); } public Ft.Value apply (Ft.Value value_1, Ft.Value value_2) throws Ft.ExpressionError { switch (this) { case AND: return apply_and (value_1, value_2); case OR: return apply_or (value_1, value_2); case EQ: return value_1.apply_eq (value_2); case LT: return value_1.apply_lt (value_2); case LTE: return apply_or (value_1.apply_lt (value_2), value_1.apply_eq (value_2)); case GT: return value_1.apply_gt (value_2); case GTE: return apply_or (value_1.apply_gt (value_2), value_1.apply_eq (value_2)); case NOT_EQ: return new Ft.BooleanValue (!value_1.apply_eq (value_2).to_boolean ()); default: assert_not_reached (); } } public GLib.Type get_result_type (GLib.Type value_type_1, GLib.Type value_type_2) { switch (this.get_category ()) { case Ft.OperatorCategory.COMPARISON: return typeof (Ft.BooleanValue); case Ft.OperatorCategory.LOGICAL: return typeof (Ft.BooleanValue); // TODO: the result type should vary at runtime; make it work like in javascript default: assert_not_reached (); } } public string to_string () { switch (this) { case AND: return "&&"; case OR: return "||"; case EQ: return "=="; case LT: return "<"; case LTE: return "<="; case GT: return ">"; case GTE: return ">="; case NOT_EQ: return "!="; default: assert_not_reached (); } } } public abstract class Value { public abstract GLib.Type get_value_type (); public abstract string get_type_name (); public abstract bool to_boolean (); public abstract string to_representation (); public abstract GLib.Variant to_variant (); public virtual GLib.Variant format (string name) throws Ft.ExpressionError { if (name != "") { throw new Ft.ExpressionError.INVALID (_("Unknown format \"%s\""), name); } return this.to_variant (); } internal virtual Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { throw new Ft.ExpressionError.INVALID ( "Comparison operation not supported for types '%s' and '%s'", // TODO: gettext this.get_type_name (), other.get_type_name ()); } internal virtual Ft.Value apply_gt (Ft.Value other) throws Ft.ExpressionError { throw new Ft.ExpressionError.INVALID ( "Relational operation not supported for types '%s' and '%s'", // TODO: gettext this.get_type_name (), other.get_type_name ()); } internal virtual Ft.Value apply_lt (Ft.Value other) throws Ft.ExpressionError { throw new Ft.ExpressionError.INVALID ( "Relational operation not supported for types '%s' and '%s'", // TODO: gettext this.get_type_name (), other.get_type_name ()); } } public class BooleanValue : Ft.Value { public bool data; public BooleanValue (bool value) { this.data = value; } public override GLib.Type get_value_type () { return typeof (Ft.BooleanValue); } public override string get_type_name () { return "boolean"; } public override bool to_boolean () { return this.data; } public override string to_representation () { return this.data.to_string (); } public override GLib.Variant to_variant () { return new GLib.Variant.boolean (this.data); } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { var other_boolean = other as Ft.BooleanValue; if (other_boolean != null) { return new Ft.BooleanValue (this.data == other_boolean.data); } return base.apply_eq (other); } } public class TimestampValue : Ft.Value { public int64 data; public TimestampValue (int64 timestamp) { this.data = timestamp; } public override GLib.Type get_value_type () { return typeof (Ft.TimestampValue); } public override string get_type_name () { return "timestamp"; } public override bool to_boolean () { return Ft.Timestamp.is_defined (this.data); } public override string to_representation () { // TODO: figure out how to represent undefined timestamp; just use null? return quote_string (this.to_string ()); } public override GLib.Variant to_variant () { return new GLib.Variant.int64 (this.data); } public override GLib.Variant format (string name) throws Ft.ExpressionError { switch (name) { case "iso8601": return new GLib.Variant.string (Ft.Timestamp.to_iso8601 (this.data)); case "seconds": return new GLib.Variant.double (Ft.Timestamp.to_seconds (this.data)); case "microseconds": return this.to_variant (); default: return base.format (name); } } private inline string to_string () { return Ft.Timestamp.to_iso8601 (this.data); } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { var other_timestamp = other as Ft.TimestampValue; if (other_timestamp != null) { return new Ft.BooleanValue (this.data == other_timestamp.data); } var other_string = other as Ft.StringValue; if (other_string != null) { return new Ft.BooleanValue (this.to_string () == other_string.data); } return base.apply_eq (other); } internal override Ft.Value apply_lt (Ft.Value other) throws Ft.ExpressionError { var other_timestamp = other as Ft.TimestampValue; if (other_timestamp != null) { return new Ft.BooleanValue (this.data < other_timestamp.data); } return base.apply_lt (other); } internal override Ft.Value apply_gt (Ft.Value other) throws Ft.ExpressionError { var other_timestamp = other as Ft.TimestampValue; if (other_timestamp != null) { return new Ft.BooleanValue (this.data > other_timestamp.data); } return base.apply_gt (other); } } public class IntervalValue : Ft.Value { public int64 data; public IntervalValue (int64 interval) { this.data = interval; } public override GLib.Type get_value_type () { return typeof (Ft.IntervalValue); } public override string get_type_name () { return "interval"; } public override bool to_boolean () { return this.data != 0; } public override string to_representation () { return this.data.to_string (); } public override GLib.Variant to_variant () { return new GLib.Variant.int64 (this.data); } public override GLib.Variant format (string name) throws Ft.ExpressionError { switch (name) { case "minutes": return new GLib.Variant.double ( Ft.Timestamp.to_seconds (this.data) / 60.0); case "seconds": return new GLib.Variant.double (Ft.Timestamp.to_seconds (this.data)); case "microseconds": return this.to_variant (); default: return base.format (name); } } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { var other_interval = other as Ft.IntervalValue; if (other_interval != null) { return new Ft.BooleanValue (this.data == other_interval.data); } return base.apply_eq (other); } internal override Ft.Value apply_lt (Ft.Value other) throws Ft.ExpressionError { var other_interval = other as Ft.IntervalValue; if (other_interval != null) { return new Ft.BooleanValue (this.data < other_interval.data); } return base.apply_lt (other); } internal override Ft.Value apply_gt (Ft.Value other) throws Ft.ExpressionError { var other_interval = other as Ft.IntervalValue; if (other_interval != null) { return new Ft.BooleanValue (this.data > other_interval.data); } return base.apply_gt (other); } } public class StringValue : Ft.Value { public string data; public StringValue (string data) { this.data = data; } public override GLib.Type get_value_type () { return typeof (Ft.StringValue); } public override string get_type_name () { return "string"; } public override bool to_boolean () { return (this.data != null) && (this.data != ""); } public override string to_representation () { return quote_string (this.data); } public override GLib.Variant to_variant () { return new GLib.Variant.string (this.data); } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { var other_string = other as Ft.StringValue; if (other_string != null) { return new Ft.BooleanValue (this.data == other_string.data); } return other.apply_eq (this); } internal override Ft.Value apply_lt (Ft.Value other) throws Ft.ExpressionError { var other_string = other as Ft.StringValue; if (other_string != null) { return new Ft.BooleanValue (this.data < other_string.data); } return other.apply_gt (this); } internal override Ft.Value apply_gt (Ft.Value other) throws Ft.ExpressionError { var other_string = other as Ft.StringValue; if (other_string != null) { return new Ft.BooleanValue (this.data > other_string.data); } return other.apply_lt (this); } } public class StateValue : Ft.Value { public Ft.State data; public StateValue (Ft.State state) { this.data = state; } public override GLib.Type get_value_type () { return typeof (Ft.StateValue); } public override string get_type_name () { return "state"; } public override bool to_boolean () { return this.data != Ft.State.STOPPED; } public override string to_representation () { return quote_string (this.data.to_string ()); } public override GLib.Variant to_variant () { return new GLib.Variant.string (this.data.to_string ()); } public override GLib.Variant format (string name) throws Ft.ExpressionError { switch (name) { case "base": return new GLib.Variant.string (this.data.is_break () ? "break" : this.data.to_string ()); case "full": return this.to_variant (); default: return base.format (name); } } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { /** * XXX: When serialized, we allow comparisons like `"short-break" == "break"`, * which is questionable. It would be more intuitive to use functions, * for this example `isBreak("short-break")`. */ var other_state = other as Ft.StateValue; if (other_state != null) { return new Ft.BooleanValue (this.data.is_a (other_state.data)); } var other_string = other as Ft.StringValue; if (other_string != null) { var other_data = Ft.State.from_string (other_string.data); if (other_data == Ft.State.STOPPED && other_string.data != "stopped") { return new Ft.BooleanValue (false); } return new Ft.BooleanValue (this.data.is_a (other_data)); } return base.apply_eq (other); } } public class StatusValue : Ft.Value { public Ft.TimeBlockStatus data; public StatusValue (Ft.TimeBlockStatus status) { this.data = status; } public override GLib.Type get_value_type () { return typeof (Ft.StatusValue); } public override string get_type_name () { return "status"; } public override bool to_boolean () { return this.data != Ft.TimeBlockStatus.SCHEDULED; } public override string to_representation () { return quote_string (this.data.to_string()); } public override GLib.Variant to_variant () { return new GLib.Variant.string (this.data.to_string ()); } internal override Ft.Value apply_eq (Ft.Value other) throws Ft.ExpressionError { var other_status = other as Ft.StatusValue; if (other_status != null) { return new Ft.BooleanValue (this.data == other_status.data); } var other_string = other as Ft.StringValue; if (other_string != null) { return new Ft.BooleanValue (this.data.to_string () == other_string.data); } return base.apply_eq (other); } } public string[] list_value_formats (GLib.Type value_type) { switch (value_type.name ()) { case "FtTimestampValue": return { "iso8601", "seconds", "microseconds" }; case "FtIntervalValue": return { "minutes", "seconds", "microseconds" }; case "FtStateValue": return { "base", "full" }; default: return {}; } } public string get_default_value_format (GLib.Type value_type) { switch (value_type.name ()) { case "FtTimestampValue": return "microseconds"; case "FtIntervalValue": return "microseconds"; case "FtStateValue": return "full"; default: return ""; } } private bool get_inner_operator (Ft.Expression expression, out Ft.Operator inner_operator) { if (expression is Ft.Operation) { inner_operator = ((Ft.Operation) expression).operator; return true; } if (expression is Ft.Comparison) { inner_operator = ((Ft.Comparison) expression).operator; return true; } inner_operator = Ft.Operator.INVALID; return false; } /** * Wrap argument with parentheses if inner operator has higher priority */ private inline string wrap_argument (string argument_string, Ft.Operator inner_operator, Ft.Operator outer_operator) { return inner_operator != outer_operator && inner_operator.get_precedence () < outer_operator.get_precedence () ? @"($argument_string)" : argument_string; } public abstract class Expression { public abstract GLib.Type get_result_type (); public abstract Ft.Value evaluate (Ft.Context context) throws Ft.ExpressionError; public abstract string to_string (); public static Ft.Expression? parse (string text) throws Ft.ExpressionParserError { var parser = new Ft.ExpressionParser (); return parser.parse (text); } } public class Constant : Ft.Expression { public Ft.Value value; public Constant (Ft.Value value) { this.value = value; } public override GLib.Type get_result_type () { return this.value.get_value_type (); } public override Ft.Value evaluate (Ft.Context context) throws Ft.ExpressionError { return this.value; } public override string to_string () { return this.value.to_representation (); } public inline string get_string () { var string_value = this.value as Ft.StringValue; return string_value != null ? string_value.data : ""; } } public class Variable : Ft.Expression { public string name; public Variable (string name) { this.name = name; } public override GLib.Type get_result_type () { var variable_spec = Ft.find_variable (this.name); return variable_spec?.value_type; } public override Ft.Value evaluate (Ft.Context context) throws Ft.ExpressionError { var value = context.evaluate_variable (this.name); if (value == null) { throw new Ft.ExpressionError.INVALID (_("Unknown variable \"%s\""), this.name); } return value; } public override string to_string () { return Ft.to_camel_case (this.name); } } public class Operation : Ft.Expression { public Ft.Operator operator; public Ft.Expression[] arguments; public Operation (Ft.Operator operator, ...) { var arguments_list = va_list (); var arguments = new Ft.Expression[0]; while (true) { Ft.Expression? argument = arguments_list.arg (); if (argument != null) { arguments += argument; } else { break; } } this.operator = operator; this.arguments = arguments; } public Operation.with_argv (Ft.Operator operator, owned Ft.Expression[] arguments) { this.operator = operator; this.arguments = arguments; } public override GLib.Type get_result_type () { GLib.Type? result_type = null; foreach (var expression in this.arguments) { if (result_type == null) { result_type = expression.get_result_type (); } else { result_type = this.operator.get_result_type (result_type, expression.get_result_type ()); } } return result_type; } public override Ft.Value evaluate (Ft.Context context) throws Ft.ExpressionError { if (this.arguments.length == 0) { throw new Ft.ExpressionError.EMPTY ("No arguments to perform '%s' operation", this.operator.to_string ()); } Ft.Value? result = null; foreach (var expression in this.arguments) { var expression_result = expression.evaluate (context); if (result == null) { result = expression_result; } else { result = this.operator.apply (result, expression_result); } } return result; } private string argument_to_string (Ft.Expression expression) { var expression_string = expression.to_string (); Ft.Operator expression_operator; return get_inner_operator (expression, out expression_operator) ? wrap_argument (expression_string, expression_operator, this.operator) : expression_string; } public override string to_string () { if (this.arguments.length != 1) { var string_builder = new GLib.StringBuilder (); var operator_string = @" $(this.operator.to_string()) "; for (var index = 0; index < this.arguments.length; index++) { if (index > 0) { string_builder.append (operator_string); } string_builder.append (argument_to_string (this.arguments[index])); } return string_builder.str; } else { return this.arguments[0].to_string (); } } } public class Comparison : Ft.Expression { public Ft.Expression argument_lhs; public Ft.Expression? argument_rhs; public Ft.Operator operator; public Comparison (Ft.Expression argument_lhs, Ft.Operator operator, Ft.Expression? argument_rhs) { this.argument_lhs = argument_lhs; this.operator = operator; this.argument_rhs = argument_rhs; } public Comparison.is_true (Ft.Expression expression) { this.argument_lhs = expression; this.operator = Ft.Operator.EQ; this.argument_rhs = new Ft.Constant (new Ft.BooleanValue (true)); } public override GLib.Type get_result_type () { return typeof (Ft.BooleanValue); } public override Ft.Value evaluate (Ft.Context context) throws Ft.ExpressionError { if (this.operator.get_category () != Ft.OperatorCategory.COMPARISON) { throw new Ft.ExpressionError.INVALID ("Expecting comparison operator, not %s", this.operator.to_string ()); } if (this.argument_lhs == null || this.argument_rhs == null) { throw new Ft.ExpressionError.EMPTY ("Missing an argument for a comparison"); } var result_lhs = this.argument_lhs.evaluate (context); var result_rhs = this.argument_rhs.evaluate (context); return operator.apply (result_lhs, result_rhs); } private string argument_to_string (Ft.Expression expression) { var expression_string = expression.to_string (); return get_inner_operator (expression, null) ? @"($expression_string)" : expression_string; } private bool argument_is_true (Ft.Expression expression) { var constant = expression as Ft.Constant; if (this.operator != Ft.Operator.EQ || constant == null) { return false; } return constant != null && (constant.value is Ft.BooleanValue) && ((Ft.BooleanValue) constant.value).data; } public override string to_string () { if (this.argument_is_true (this.argument_rhs)) { return this.argument_lhs.to_string (); } if (this.argument_is_true (this.argument_lhs)) { return this.argument_rhs.to_string (); } var argument_lhs_string = this.argument_to_string (this.argument_lhs); var argument_rhs_string = this.argument_to_string (this.argument_rhs); var operator_string = this.operator.to_string (); return @"$argument_lhs_string $operator_string $argument_rhs_string"; } } } focustimerhq-FocusTimer-8581be2/src/core/gap-entry.vala000066400000000000000000000014501520625676500231510ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public class GapEntry : Gom.Resource { public int64 id { get; set; } public int64 time_block_id { get; set; } public int64 start_time { get; set; } public int64 end_time { get; set; } public string flags { get; set; } internal ulong version = 0; static construct { set_table ("gaps"); set_primary_key ("id"); set_notnull ("time-block-id"); set_notnull ("start-time"); set_notnull ("end-time"); set_notnull ("flags"); set_unique ("start-time"); set_reference ("time-block-id", "timeblocks", "id"); } } } focustimerhq-FocusTimer-8581be2/src/core/gap.vala000066400000000000000000000210141520625676500220100ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { [Flags] public enum GapFlags { DEFAULT = 0, INTERRUPTION = 1, SLEEP = 2; public static Ft.GapFlags from_string (string? str) { var result = DEFAULT; if (str == null) { return result; } foreach (var part in str.split ("|")) { switch (part.strip ()) { case "default": break; case "interruption": result |= INTERRUPTION; break; case "sleep": result |= SLEEP; break; default: GLib.warning ("Unknown `GapFlags` value '%s'", part.strip ()); break; } } return result; } public string to_string () { string[] strings_array = {}; if ((this & INTERRUPTION) > 0) { strings_array += "interruption"; } if ((this & SLEEP) > 0) { strings_array += "sleep"; } return strings_array.length > 0 ? string.joinv (" | ", strings_array) : "default"; } } public class Gap : GLib.InitiallyUnowned, Ft.Schedulable { [CCode (notify = false)] public int64 start_time { get { return this._start_time; } set { if (this._start_time == value) { return; } if (Ft.Timestamp.is_undefined (value) || Ft.Timestamp.is_undefined (this._end_time) || value <= this._end_time) { this.set_time_range (value, this._end_time); } else { this.set_time_range (value, value); } } } [CCode (notify = false)] public int64 end_time { get { return this._end_time; } set { if (this._end_time == value) { return; } if (Ft.Timestamp.is_undefined (value) || Ft.Timestamp.is_undefined (this._start_time) || value >= this._start_time) { this.set_time_range (this._start_time, value); } else { this.set_time_range (value, value); } } } /** * `duration` of a time block, including gaps */ [CCode (notify = false)] public int64 duration { get { return Ft.Timestamp.subtract (this._end_time, this._start_time); } set { if (Ft.Timestamp.is_defined (this._start_time)) { this.set_time_range (this._start_time, Ft.Timestamp.add_interval (this._start_time, value)); } else { GLib.warning ("Can't change time-block duration without a defined start-time."); } } } public Ft.GapFlags flags { get { return this._flags; } set { if (this._flags != value) { this._flags = value; this.version++; } } } public weak Ft.TimeBlock time_block { get; set; } // parent internal ulong version = 0; internal Ft.GapEntry? entry = null; private int64 _start_time = Ft.Timestamp.UNDEFINED; private int64 _end_time = Ft.Timestamp.UNDEFINED; private Ft.GapFlags _flags = Ft.GapFlags.DEFAULT; public Gap (Ft.GapFlags flags = Ft.GapFlags.DEFAULT) { GLib.Object ( flags: flags ); } public Gap.with_start_time (int64 start_time, Ft.GapFlags flags = Ft.GapFlags.DEFAULT) { GLib.Object ( flags: flags ); this.set_time_range (start_time, this._end_time); } public inline bool has_flag (Ft.GapFlags flag) { return (this._flags & flag) == flag; } public inline void set_flag (Ft.GapFlags flag) { this.flags |= flag; } public inline void unset_flag (Ft.GapFlags flag) { this.flags = this._flags & (~flag); } private void emit_changed () { this.version++; this.changed (); } public void set_time_range (int64 start_time, int64 end_time) { var old_start_time = this._start_time; var old_end_time = this._end_time; var old_duration = this._end_time - this._start_time; var changed = false; this._start_time = start_time; this._end_time = end_time; if (this._start_time != old_start_time) { this.notify_property ("start-time"); changed = true; } if (this._end_time != old_end_time) { this.notify_property ("end-time"); changed = true; } if (this._end_time - this._start_time != old_duration) { this.notify_property ("duration"); } if (changed) { this.emit_changed (); } } public void move_by (int64 offset) { var start_time = Ft.Timestamp.is_defined (this._start_time) ? Ft.Timestamp.add_interval (this._start_time, offset) : Ft.Timestamp.UNDEFINED; var end_time = Ft.Timestamp.is_defined (this._end_time) ? Ft.Timestamp.add_interval (this._end_time, offset) : Ft.Timestamp.UNDEFINED; this.set_time_range (start_time, end_time); } public void move_to (int64 start_time) { if (Ft.Timestamp.is_undefined (this._start_time) && Ft.Timestamp.is_undefined (this._end_time)) { this.set_time_range (start_time, this._end_time); return; } if (Ft.Timestamp.is_undefined (this._start_time)) { GLib.warning ("Unable to move gap. Gap start-time is undefined."); return; } this.move_by (Ft.Timestamp.subtract (start_time, this._start_time)); } /* * Database */ internal bool should_create_entry () { return Ft.Timestamp.is_defined (this.start_time); } internal bool should_update_entry () { if (this.entry == null || this.entry.id == 0) { return true; } return this.entry.version != this.version; } internal unowned Ft.GapEntry create_or_update_entry () requires (this.time_block.entry != null) { if (this.entry == null) { this.entry = new Ft.GapEntry (); this.entry.repository = Ft.Database.get_repository (); this.time_block.entry.bind_property ("id", this.entry, "time-block-id", GLib.BindingFlags.SYNC_CREATE); } this.entry.start_time = this._start_time; this.entry.end_time = this._end_time; this.entry.flags = this._flags.to_string (); this.entry.version = this.version; return this.entry; } internal void unset_entry () { this.entry = null; } /* * Signals */ public signal void changed (); public override void dispose () { this.time_block = null; this.entry = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/idle-monitor.vala000066400000000000000000000316011520625676500236460ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public delegate void Callback (); public interface IdleMonitorProvider : Ft.Provider { public abstract bool can_ignore_inhibitors { get; } /** * Register an idle watch with the session idle monitor. * * Fires `became_idle` after `timeout` microseconds without user activity. * * When `monotonic_time` is defined, idle time is counted from from the given timestamp. * * When `ignore_inhibitors` is `true`, the watch reacts to real user input (not supported * by all providers). */ public abstract uint32 add_idle_watch (int64 timeout, bool ignore_inhibitors, int64 monotonic_time) throws GLib.Error; /** * Reschedule an idle watch. It ensures that it'll not fire before `timeout` passes * counting from the current time. */ public abstract uint32 reset_idle_watch (uint32 id, int64 monotonic_time) throws GLib.Error; public abstract void remove_idle_watch (uint32 id) throws GLib.Error; public abstract void add_active_watch () throws GLib.Error; public abstract void remove_active_watch () throws GLib.Error; public signal void became_idle (uint32 id); public signal void became_active (); /** * Convert an idle interval anchored at `reference_time` into one relative to the * user's last activity. */ public static int64 calculate_absolute_timeout (int64 relative_timeout, int64 idle_time, int64 reference_time) requires (Ft.Timestamp.is_defined (reference_time)) { if (idle_time == 0) { return relative_timeout; } var last_active_time = GLib.get_monotonic_time () - idle_time; var absolute_timeout = relative_timeout + reference_time - last_active_time; return absolute_timeout > 0 ? absolute_timeout : relative_timeout; } } // TODO: should be defined in tests public class DummyIdleMonitorProvider : Ft.Provider, Ft.IdleMonitorProvider { public bool can_ignore_inhibitors { get { return false; } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.available = true; this.enabled = true; // HACK: This is to skip the need for a main loop in tests } public override async void uninitialize () throws GLib.Error { this.available = false; } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { } public override async void disable () throws GLib.Error { } public int64 get_idle_time () throws GLib.Error { return 0; } public uint32 add_idle_watch (int64 timeout, bool ignore_inhibitors, int64 monotonic_time) throws GLib.Error { return 1; } public uint32 reset_idle_watch (uint32 id, int64 monotonic_time) throws GLib.Error { return 1; } public void add_active_watch () throws GLib.Error { } public void remove_idle_watch (uint32 id) throws GLib.Error { } public void remove_active_watch () throws GLib.Error { } } [SingleInstance] public class IdleMonitor : Ft.ProvidedObject { private static uint next_watch_id = 1U; [Compact] private class Watch { public uint id = 0U; public uint32 external_id = 0U; public int64 timeout = 0; public int64 reference_time = Ft.Timestamp.UNDEFINED; public bool ignore_inhibitors = false; public Ft.Callback? idle_callback = null; public Ft.Callback? active_callback = null; public unowned Ft.IdleMonitorProvider? provider = null; public bool invalid = false; ~Watch () { this.idle_callback = null; this.active_callback = null; this.provider = null; } } private GLib.HashTable watches = null; private int64 last_activity_time = Ft.Timestamp.UNDEFINED; private void on_became_idle (uint32 id) { // We don't expect idle watch to be called often, so linear scan is good enough. unowned Watch? watch = this.watches.find ( (_id, watch) => { return watch.external_id == id; }); if (watch != null && !watch.invalid) { watch.idle_callback (); } } private void on_became_active () { var monotonic_time = GLib.get_monotonic_time (); this.last_activity_time = monotonic_time; (unowned Watch)[] watches_to_trigger = new Watch[0]; this.watches.@foreach ( (id, watch) => { if (watch.invalid) { return; } if (watch.active_callback != null) { watches_to_trigger += watch; } if (watch.idle_callback != null) { try { // Let the provider decide whether internally it needs a reset. watch.external_id = this.provider.reset_idle_watch (watch.external_id, monotonic_time); } catch (GLib.Error error) { GLib.warning ("Unable to reset an idle-watch: %s", error.message); return; } } }); for (var index = 0; index < watches_to_trigger.length; index++) { unowned Watch watch = watches_to_trigger[index]; watch.active_callback (); watch.invalid = true; } this.watches.foreach_remove ( (id, watch) => { return watch.invalid && watch.active_callback != null; }); } protected override void initialize () { // Initialize here rather than in `construct` block, as base `construct` runs first // and may trigger `provider_enabled()` which accesses `watches`. this.watches = new GLib.HashTable (GLib.int64_hash, GLib.int64_equal); } protected override void setup_providers () { if (Ft.is_test ()) { this.providers.add (new Ft.DummyIdleMonitorProvider (), Ft.Priority.HIGH); } } protected override void provider_enabled (Ft.IdleMonitorProvider provider) { provider.became_idle.connect (this.on_became_idle); provider.became_active.connect (this.on_became_active); // Recreate watches with the new provider. this.watches.@foreach ( (id, watch) => { try { watch.external_id = provider.add_idle_watch ( watch.timeout, watch.ignore_inhibitors && provider.can_ignore_inhibitors, watch.reference_time); } catch (GLib.Error error) { GLib.warning ("Error while adding idle watch: %s", error.message); } }); } protected override void provider_disabled (Ft.IdleMonitorProvider provider) { provider.became_idle.disconnect (this.on_became_idle); provider.became_active.disconnect (this.on_became_active); this.watches.@foreach ( (id, watch) => { try { provider.remove_idle_watch (watch.external_id); } catch (GLib.Error error) { GLib.warning ("Error while removing idle watch: %s", error.message); } watch.external_id = 0; }); } /** * Register an idle watch. * * `reference_time` specifies whether idle-time should be detected from this point of time, * otherwise the callback will be called counting from the time of users last activity. */ public uint add_idle_watch (int64 timeout, bool ignore_inhibitors, owned Ft.Callback callback, int64 monotonic_time = Ft.Timestamp.UNDEFINED) { if (timeout == 0) { return 0; } var watch_id = Ft.IdleMonitor.next_watch_id; Ft.IdleMonitor.next_watch_id++; var watch = new Watch (); watch.id = watch_id; watch.timeout = timeout; watch.ignore_inhibitors = ignore_inhibitors; watch.idle_callback = (owned) callback; watch.reference_time = monotonic_time; watch.provider = this.provider; if (this.provider != null && this.provider.enabled) { try { watch.external_id = this.provider.add_idle_watch ( timeout, ignore_inhibitors && this.provider.can_ignore_inhibitors, monotonic_time); } catch (GLib.Error error) { GLib.warning ("Unable to add an idle-watch: %s", error.message); } } else { GLib.debug ("Unable to add an idle-watch: no provider."); } this.watches.insert (watch_id, (owned) watch); return watch_id; } /** * Trigger callback on first user activity counting from now. */ public uint add_active_watch (owned Ft.Callback callback, int64 monotonic_time = Ft.Timestamp.UNDEFINED) { var watch_id = Ft.IdleMonitor.next_watch_id; Ft.IdleMonitor.next_watch_id++; var watch = new Watch (); watch.id = watch_id; watch.active_callback = (owned) callback; watch.reference_time = monotonic_time; watch.provider = this.provider; if (this.provider != null && this.provider.enabled) { try { this.provider.add_active_watch (); } catch (GLib.Error error) { GLib.warning ("Unable to add an active-watch: %s", error.message); } } else { GLib.debug ("Unable to add an active-watch: no provider."); } this.watches.insert (watch_id, (owned) watch); return watch_id; } public void remove_watch (uint id) { unowned Watch? watch = this.watches.lookup (id); if (watch == null) { return; } if (this.provider == null) { watch.invalid = true; return; } try { if (watch.idle_callback != null && watch.external_id != 0) { this.provider.remove_idle_watch (watch.external_id); } if (watch.active_callback != null) { this.provider.remove_active_watch (); } this.watches.remove (id); } catch (GLib.Error error) { GLib.debug ("Error while removing watch: %s", error.message); watch.invalid = true; } } public override void dispose () { base.dispose (); // Watches are needed for destroying providers during `base.dispose()`. this.watches = null; } } } focustimerhq-FocusTimer-8581be2/src/core/indicator.vala000066400000000000000000000045421520625676500232240ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface IndicatorProvider : Ft.Provider { public abstract bool visible { get; } } /** * A helper primitive to ensure only one indicator is enabled at a time. * * Also, to run the app in background when an indicator is available. */ [SingleInstance] public class Indicator : Ft.ProvidedObject { private Ft.BackgroundManager? background_manager = null; private uint background_hold_id = 0U; private void update_background_hold () { if (this.provider != null && this.provider.enabled && this.provider.visible) { if (this.background_hold_id == 0U) { this.background_hold_id = this.background_manager.hold_sync (); } } else { if (this.background_hold_id != 0U) { this.background_manager.release (this.background_hold_id); this.background_hold_id = 0; } } } private void on_notify_visible (GLib.Object obj, GLib.ParamSpec pspec) { this.update_background_hold (); } protected override void initialize () { this.background_manager = new Ft.BackgroundManager (); } protected override void setup_providers () { } protected override void provider_enabled (Ft.IndicatorProvider provider) { provider.notify["visible"].connect (this.on_notify_visible); this.update_background_hold (); } protected override void provider_disabled (Ft.IndicatorProvider provider) { provider.notify["visible"].disconnect (this.on_notify_visible); this.update_background_hold (); } public override void dispose () { if (this.background_hold_id != 0U) { this.background_manager.release (this.background_hold_id); this.background_hold_id = 0U; } this.background_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/job-queue.vala000066400000000000000000000053641520625676500231470ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public interface Job : GLib.Object { public abstract bool completed { get; set; default = false; } public abstract GLib.Error? error { get; set; default = null; } public abstract async bool run () throws GLib.Error; } /** * Run scheduled jobs sequentially. Currently we only run one type of jobs - actions / commands. * If there were more types, likely we would need to manage several queues / workers. */ [SingleInstance] public class JobQueue : GLib.Object { private GLib.AsyncQueue queue; private bool running = false; construct { this.queue = new GLib.AsyncQueue (); } private void run_job (Ft.Job job) requires (!this.running) { this.running = true; job.run.begin ( (obj, res) => { try { job.run.end (res); } catch (GLib.Error error) { assert (job.error != null); } if (!job.completed) { GLib.warning ("Job %s did not complete", job.get_type ().name ()); } this.running = false; this.pop (false); if (!this.running && this.queue.length () == 0) { this.drained (); } }); } private void pop (bool may_block) { var job = may_block ? this.queue.pop () : this.queue.try_pop (); if (job != null) { this.run_job (job); } } public void push (Ft.Job job) { this.queue.push (job); if (!this.running) { this.pop (false); } } /** * Wait until all jobs are completed. */ public async void wait () { ulong drained_id = 0; if (!this.running && this.queue.length () == 0) { return; } drained_id = this.drained.connect (() => { this.disconnect (drained_id); wait.callback (); }); yield; } /* * Emitted when the queue is empty and all jobs have completed. */ public signal void drained (); public override void dispose () { this.queue = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/keyboard-manager.vala000066400000000000000000000166771520625676500244740ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public interface GlobalShortcutsProvider : Ft.Provider { public abstract void add_shortcut (string name, string description, string default_accelerator = ""); public abstract string lookup_accelerator (string name); public abstract void open_global_shortcuts_dialog (string window_identifier); public signal void shortcut_activated (string name); public signal void accelerator_changed (string name); } public delegate void ForeachAcceleratorFunc (string shortcut_name, string shortcut_accelerator); [SingleInstance] public class KeyboardManager : GLib.Object { [Compact] private class Shortcut { public string name; public string description; public string default_accelerator; public Shortcut (string name, string description, string default_accelerator) { this.name = name; this.description = description; this.default_accelerator = default_accelerator; } ~Shortcut () { this.name = null; this.description = null; this.default_accelerator = null; } } public bool global_shortcuts_supported { get { return this._global_shortcuts_supported; } } private Ft.ProviderSet providers; private unowned Ft.GlobalShortcutsProvider? provider = null; private GLib.HashTable shortcuts = null; private bool inhibited = false; private bool _global_shortcuts_supported = false; construct { this.shortcuts = new GLib.HashTable (GLib.str_hash, GLib.str_equal); this.providers = new Ft.ProviderSet ( Ft.SelectionMode.SINGLE); this.providers.provider_selected.connect (this.on_provider_selected); this.providers.provider_unselected.connect (this.on_provider_unselected); this.providers.provider_enabled.connect (this.on_provider_enabled); this.providers.provider_disabled.connect (this.on_provider_disabled); this.providers.discover (); this.providers.enable (); } private void populate_provider () requires (this.provider != null) { this.shortcuts.@foreach ( (shortcut_name, shortcut) => { this.provider.add_shortcut (shortcut.name, shortcut.description, shortcut.default_accelerator); }); } private void on_provider_notify_available (GLib.Object object, GLib.ParamSpec pspec) { var provider = (Ft.GlobalShortcutsProvider) object; if (!this._global_shortcuts_supported && provider.available) { this._global_shortcuts_supported = true; this.notify_property ("global-shortcuts-supported"); } } private void on_shortcut_activated (string shortcut_name) { if (!this.inhibited) { this.shortcut_activated (shortcut_name); } } private void on_accelerator_changed (string shortcut_name) { this.shortcut_changed (shortcut_name); } private void on_provider_selected (Ft.GlobalShortcutsProvider provider) { provider.notify["available"].connect (this.on_provider_notify_available); provider.shortcut_activated.connect (this.on_shortcut_activated); provider.accelerator_changed.connect (this.on_accelerator_changed); if (!this._global_shortcuts_supported && provider.available) { this._global_shortcuts_supported = true; this.notify_property ("global-shortcuts-supported"); } } private void on_provider_unselected (Ft.GlobalShortcutsProvider provider) { provider.notify["available"].disconnect (this.on_provider_notify_available); provider.shortcut_activated.disconnect (this.on_shortcut_activated); provider.accelerator_changed.disconnect (this.on_accelerator_changed); if (this._global_shortcuts_supported) { this._global_shortcuts_supported = false; this.notify_property ("global-shortcuts-supported"); } } private void on_provider_enabled (Ft.GlobalShortcutsProvider provider) { this.provider = provider; if (this.provider != null) { this.populate_provider (); } } private void on_provider_disabled (Ft.GlobalShortcutsProvider provider) { if (this.provider == provider) { this.provider = null; } } public void add_shortcut (string name, string description, string default_accelerator = "") { var shortcut = new Shortcut (name, description, default_accelerator); this.shortcuts.insert (name, (owned) shortcut); if (this.provider != null && this.provider.enabled) { this.provider.add_shortcut (name, description, default_accelerator); } } /** * Opens a system dialog for editing shortcuts. */ public void open_global_shortcuts_dialog (string window_identifier = "") { if (this.provider != null) { this.provider.open_global_shortcuts_dialog (window_identifier); } } public void inhibit () { this.inhibited = true; } public void uninhibit () { this.inhibited = false; } public string lookup_accelerator (string shortcut_name) { return this.provider != null ? this.provider.lookup_accelerator (shortcut_name) : ""; } public void foreach_accelerator (ForeachAcceleratorFunc func) { this.shortcuts.@foreach ( (shortcut_name, shortcut) => { func (shortcut_name, this.lookup_accelerator (shortcut_name)); }); } public signal void shortcut_activated (string shortcut_name); public signal void shortcut_changed (string shortcut_name); public override void dispose () { this.providers = null; this.provider = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/locale.vala000066400000000000000000000112471520625676500225070ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ [CCode (cprefix = "")] namespace Ft.Locale { #if HAVE_ALTMON [CCode (cheader_filename = "langinfo.h", cprefix = "", has_type_id = false)] private enum NLItem { ALTMON_1, ALTMON_2, ALTMON_3, ALTMON_4, ALTMON_5, ALTMON_6, ALTMON_7, ALTMON_8, ALTMON_9, ALTMON_10, ALTMON_11, ALTMON_12; [CCode (cheader_filename = "langinfo.h", cname = "nl_langinfo")] public extern unowned string to_string (); } private const NLItem[] MONTHS = { NLItem.ALTMON_1, NLItem.ALTMON_2, NLItem.ALTMON_3, NLItem.ALTMON_4, NLItem.ALTMON_5, NLItem.ALTMON_6, NLItem.ALTMON_7, NLItem.ALTMON_8, NLItem.ALTMON_9, NLItem.ALTMON_10, NLItem.ALTMON_11, NLItem.ALTMON_12 }; #else private const Posix.NLItem[] MONTHS = { Posix.NLItem.MON_1, Posix.NLItem.MON_2, Posix.NLItem.MON_3, Posix.NLItem.MON_4, Posix.NLItem.MON_5, Posix.NLItem.MON_6, Posix.NLItem.MON_7, Posix.NLItem.MON_8, Posix.NLItem.MON_9, Posix.NLItem.MON_10, Posix.NLItem.MON_11, Posix.NLItem.MON_12 }; #endif private GLib.DateWeekday first_day_of_week = GLib.DateWeekday.BAD_WEEKDAY; /** * Based on gtkcalendar.c and https://sourceware.org/glibc/wiki/Locales */ public GLib.DateWeekday get_first_day_of_week () { if (!first_day_of_week.valid ()) { // `nl_langinfo(_NL_TIME_WEEK_1STDAY)` returns a pointer whose VALUE encodes // the date. GTK uses a union to extract the lower 32 bits. We do the same via casting. unowned string week_origin_ptr = Posix.NLTime.WEEK_1STDAY.to_string (); var week_1stday = 0; if (week_origin_ptr != null) { // Extract lower 32 bits of the pointer value (like GTK's union trick) // The pointer value itself encodes the date on little-endian systems var ptr_value = (size_t) ((void*) week_origin_ptr); var week_origin = (uint32) (ptr_value & 0xFFFFFFFF); if (week_origin == 19971130) { // Sunday week_1stday = 0; } else if (week_origin == 19971201) { // Monday week_1stday = 1; } else { GLib.warning ("Unknown value of _NL_TIME_WEEK_1STDAY: %u", week_origin); } } // FIRST_WEEKDAY is different from WEEK_1STDAY - it returns a pointer to a string // containing a byte value (1-7) unowned string first_weekday_str = Posix.NLTime.FIRST_WEEKDAY.to_string (); var first_weekday = 1; if (first_weekday_str != null && first_weekday_str.length > 0) { // Read the first byte (like GTK does with langinfo.string[0]) var weekday_byte = (uint8) first_weekday_str[0]; if (weekday_byte >= 1 && weekday_byte <= 7) { first_weekday = (int) weekday_byte; } else { GLib.warning ("Unexpected _NL_TIME_FIRST_WEEKDAY byte value: %u", weekday_byte); } } first_day_of_week = (week_1stday + first_weekday - 1) % 7 == 0 ? GLib.DateWeekday.SUNDAY : GLib.DateWeekday.MONDAY; } return first_day_of_week; } public string get_month_name (uint month_number) { if (month_number < 1 || month_number > 12) { return ""; } var month_name = MONTHS[month_number - 1].to_string (); // Convert to UTF-8 if needed if (!month_name.validate ()) { try { string charset; GLib.get_charset (out charset); var bytes = month_name.data; month_name = GLib.convert ((string) bytes, -1, "UTF-8", charset, null, null); } catch (GLib.ConvertError error) { GLib.warning ("Failed to convert month name to UTF-8: %s", error.message); } } return month_name; } /** * Return whether to prefer 12h (AM/PM) format. */ public bool use_12h_format () { unowned string t_fmt_ptr = Posix.NLItem.T_FMT.to_string (); return t_fmt_ptr != null ? t_fmt_ptr.ascii_casecmp ("%I") == 0 : false; } } focustimerhq-FocusTimer-8581be2/src/core/lock-screen.vala000066400000000000000000000035301520625676500234510ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface LockScreenProvider : Ft.Provider { public abstract bool active { get; } public abstract void activate (); } [SingleInstance] public class LockScreen : Ft.ProvidedObject { public bool active { get { return this._active; } } private bool _active = false; private void update_active () { var active = this.provider != null && this.provider.enabled ? this.provider.active : false; if (this._active != active) { this._active = active; this.notify_property ("active"); } } private void on_notify_active (GLib.Object object, GLib.ParamSpec pspec) { this.update_active (); } protected override void initialize () { } protected override void setup_providers () { } protected override void provider_enabled (Ft.LockScreenProvider provider) { provider.notify["active"].connect (this.on_notify_active); this.update_active (); } protected override void provider_disabled (Ft.LockScreenProvider provider) { provider.notify["active"].disconnect (this.on_notify_active); this.update_active (); } public void activate () { if (this.provider != null && this.provider.enabled) { this.provider.activate (); } else { GLib.debug ("Unable to activate lock-screen: no provider"); } } } } focustimerhq-FocusTimer-8581be2/src/core/logger.vala000066400000000000000000000162251520625676500225300ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public abstract class LogEntry : GLib.Object { public ulong id { get { return this._id; } construct { this._id = Ft.LogEntry.next_id; Ft.LogEntry.next_id++; } } public int64 timestamp { get; set; } public string label { get; set; } public Ft.Context context { get; set; } private ulong _id; private static ulong next_id = 1; } public sealed class EventLogEntry : Ft.LogEntry { public string event_name { get { return this._event_name; } set { this._event_name = value; } } private string _event_name; public EventLogEntry (Ft.Event event) { GLib.Object ( timestamp: event.context.timestamp, label: event.spec.display_name, context: event.context, event_name: event.spec.name ); } } public class ActionLogEntry : Ft.LogEntry { public string action_uuid { get { return this._action_uuid; } set { this._action_uuid = value; } } public string event_name { get { return this._event_name; } set { this._event_name = value; } } public string command_line { get { return this._command_line; } set { this._command_line = value; } } public string command_output { get { return this._command_output; } set { this._command_output = value; } } public string command_error_message { get; set; } public int command_exit_code { get; set; } public int64 command_execution_time { get; set; } private string _action_uuid; private string _event_name; private string? _command_line; private string? _command_output; public ActionLogEntry (Ft.Action action, string event_name, Ft.Context context, Ft.CommandExecution? execution) { GLib.Object ( timestamp: context.timestamp, label: action.display_name, event_name: event_name, context: context, action_uuid: action.uuid, command_line: execution != null ? execution.get_line () : "", command_error_message: null, command_exit_code: -1, command_execution_time: 0 ); } } [SingleInstance] public class Logger : GLib.Object { private const uint MAX_ENTRIES = 200; public GLib.ListModel model { get { return this._model; } } private GLib.ListStore _model = null; construct { // TODO: ensure model is sorted and group entries into sections, // likely we will need custom model this._model = new GLib.ListStore (typeof (Ft.LogEntry)); } private bool transform_to_error_message (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var error = (GLib.Error?) source_value.get_boxed (); target_value.set_string (error != null ? error.message : null); return true; } private inline ulong log (Ft.LogEntry entry) { this._model.append (entry); while (this._model.n_items > MAX_ENTRIES) { this._model.remove (0); } return entry.id; } public ulong log_event (Ft.Event event) { return this.log (new Ft.EventLogEntry (event)); } private ulong log_action_event (Ft.Action action, string event_name, Ft.Context context, Ft.CommandExecution? execution) { var entry = new Ft.ActionLogEntry (action, event_name, context, execution); if (execution != null) { execution.bind_property ("output", entry, "command-output", GLib.BindingFlags.SYNC_CREATE); execution.bind_property ("exit-code", entry, "command-exit-code", GLib.BindingFlags.SYNC_CREATE); execution.bind_property ("execution-time", entry, "command-execution-time", GLib.BindingFlags.SYNC_CREATE); execution.bind_property ("error", entry, "command-error-message", GLib.BindingFlags.SYNC_CREATE, this.transform_to_error_message); if (!execution.completed) { // HACK: We use `command-exit-code` to tell if job has completed. // This won't work if there's a validation error. execution.notify["completed"].connect ( () => { if (entry.command_exit_code < 0) { entry.notify_property ("command-exit-code"); } }); } } return this.log (entry); } public ulong log_action_triggered (Ft.EventAction action, Ft.Context context, Ft.CommandExecution? execution) { return this.log_action_event (action, "triggered", context, execution); } public ulong log_action_entered_condition (Ft.ConditionAction action, Ft.Context context, Ft.CommandExecution? execution) { return this.log_action_event (action, "entered-condition", context, execution); } public ulong log_action_exited_condition (Ft.ConditionAction action, Ft.Context context, Ft.CommandExecution? execution) { return this.log_action_event (action, "exited-condition", context, execution); } public override void dispose () { this._model = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/matrix.vala000066400000000000000000000553451520625676500225630ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { /** * Class for representing a variable-size vector. * * It's just for pure convenience to have vector operations at hand. * Underneath `data` is stored as `GArray` - Vala by default does that for dynamic arrays, * on top of Vector being a compact class... It's not very efficient. * * XXX: Consider making functions for simple arrays (`double[]`) */ [Compact] // TODO: reconsider simple struct public class Vector { public double[] data; // XXX: try to store as 2D array to avoid GArray public Vector (uint length) { this.data = new double[length]; } public Vector.from_array (double[] array) { this.data = array; } ~Vector () { this.data = null; } public Ft.Vector copy () { var result = new Ft.Vector (this.data.length); for (var i = 0; i < this.data.length; i++) { result.data[i] = this.data[i]; } return result; } private inline bool validate_index (ref int i) { if (i < 0) { i = this.data.length + i; } return i < this.data.length; } public double @get (int i, double default_value = double.NAN) { return this.validate_index (ref i) ? this.data[i] : default_value; } public void @set (int i, double value) { if (this.validate_index (ref i)) { this.data[i] = value; } else { GLib.warning ("Vector index %u is out of bounds", i); } } public bool equals (Ft.Vector other) { if (this.data.length != data.length) { return false; } for (var i = 0; i < this.data.length; i++) { if ((this.data[i] - other.data[i]).abs () > double.EPSILON) { return false; } } return true; } public bool add (Ft.Vector other) { if (other.data.length != this.data.length) { return false; } for (var i = 0; i < this.data.length; i++) { this.data[i] += other.data[i]; } return true; } public double sum () { var result = 0.0; for (var i = 0; i < this.data.length; i++) { result += this.data[i]; } return result; } public double min () { var result = this.data[0]; for (var i = 0; i < this.data.length; i++) { if (result > this.data[i]) { result = this.data[i]; } } return result; } public double max () { var result = this.data[0]; for (var i = 0; i < this.data.length; i++) { if (result < this.data[i]) { result = this.data[i]; } } return result; } public string to_representation () { var string_builder = new GLib.StringBuilder (); string_builder.append ("{ "); for (var i = 0; i < this.data.length; i++) { if (i > 0) { string_builder.append (", "); } string_builder.append ("%.3g".printf (this.data[i])); } string_builder.append (" }"); return string_builder.str; } } /** * Class for representing a variable-size 2D array. It's intended for large data, that's why * it's not a `struct`. * * If you intend to do maths, consider `Gsl.Matrix` or `Cogl.Matrix`. * * API is inspired by `numpy`. */ [Compact] public class Matrix { private const int DIMENSIONS = 2; public uint[] shape; public double[,] data; public Matrix (uint shape_0, uint shape_1) { this.shape = { shape_0, shape_1 }; this.data = new double[shape_0, shape_1]; } public Matrix.from_array (double[,] array) { this.shape = { array.length[0], array.length[1] }; this.data = array; } ~Matrix () { this.shape = null; this.data = null; } public Ft.Matrix copy () { var result = new Ft.Matrix (this.shape[0], this.shape[1]); for (var i = 0; i < this.shape[0]; i++) { for (var j = 0; j < this.shape[1]; j++) { result.data[i, j] = this.data[i, j]; } } return result; } private inline bool validate_axis (ref int axis) { if (axis < 0) { axis = DIMENSIONS + axis; } return axis < DIMENSIONS; } private inline bool validate_indices (ref int i, ref int j) { if (i < 0) { i = this.data.length[0] + i; } if (j < 0) { j = this.data.length[1] + j; } return i < this.data.length[0] && j < this.data.length[1]; } private inline bool validate_index (ref int axis, ref int index) { if (!this.validate_axis (ref axis)) { return false; } if (index < 0) { index = (int) this.shape[axis] + index; } return index < (int) this.shape[axis]; } public void resize (uint shape_0, uint shape_1, double default_value = 0.0) { var intersect_0 = uint.min (shape_0, this.shape[0]); var intersect_1 = uint.min (shape_1, this.shape[1]); var data = new double[shape_0, shape_1]; if (default_value != 0.0) { for (var i = 0; i < shape_0; i++) { for (var j = 0; j < shape_1; j++) { data[i, j] = default_value; } } } for (var i = 0; i < intersect_0; i++) { for (var j = 0; j < intersect_1; j++) { data[i, j] = this.data[i, j]; } } this.data = data; this.shape = { shape_0, shape_1 }; } public double @get (int i, int j, double default_value = double.NAN) { return this.validate_indices (ref i, ref j) ? this.data[i, j] : default_value; } public void @set (int i, int j, double value) { if (this.validate_indices (ref i, ref j)) { this.data[i, j] = value; } else { GLib.warning ("Matrix indices %u, %u are out of bounds", i, j); } } public void fill (double value) { for (var i = 0; i < this.shape[0]; i++) { for (var j = 0; j < this.shape[1]; j++) { this.data[i, j] = value; } } } public bool equals (Ft.Matrix other) { if (this.shape[0] != other.shape[0] || this.shape[1] != other.shape[1]) { return false; } for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { if ((this.data[i, j] - other.data[i, j]).abs () > double.EPSILON) { return false; } } } return true; } public bool add (Ft.Matrix other) { if (other.shape[0] != this.shape[0] || other.shape[1] != this.shape[1]) { return false; } for (var i = 0; i < this.shape[0]; i++) { for (var j = 0; j < this.shape[1]; j++) { this.data[i, j] += other.data[i, j]; } } return true; } public void add_value (int i, int j, double value) { if (this.validate_indices (ref i, ref j)) { this.data[i, j] += value; } else { GLib.warning ("Matrix indices %u, %u are out of bounds", i, j); } } // TODO: can we return unowned value here? public inline Vector? get_vector_internal (int axis, int index) { switch (axis) { case 0: var data = new double[this.data.length[1]]; for (var j = 0; j < this.data.length[1]; j++) { data[j] = this.data[index, j]; } return new Ft.Vector.from_array (data); case 1: var data = new double[this.data.length[0]]; for (var i = 0; i < this.data.length[0]; i++) { data[i] = this.data[i, index]; } return new Ft.Vector.from_array (data); default: assert_not_reached (); } } public Ft.Vector? get_vector (int axis, int index) { if (!this.validate_index (ref axis, ref index)) { return null; } return this.get_vector_internal (axis, index); } public double sum () { var result = 0.0; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { result += this.data[i, j]; } } return result; } public double min () { var result = this.shape[0] > 0 && this.shape[1] > 0 ? this.data[0, 0] : double.NAN; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { if (result > this.data[i, j]) { result = this.data[i, j]; } } } return result; } public double max () { var result = this.shape[0] > 0 && this.shape[1] > 0 ? this.data[0, 0] : double.NAN; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { if (result < this.data[i, j]) { result = this.data[i, j]; } } } return result; } /** * Split an array into multiple sub-vectors along given axis. */ public Vector[] unstack (int axis = -1) { assert (this.validate_axis (ref axis)); var result = new Vector[this.shape[axis]]; for (var index = 0; index < this.shape[axis]; index++) { result[index] = this.get_vector_internal (axis, index); } return result; } public string to_representation () { var string_builder = new GLib.StringBuilder (); string_builder.append ("{\n"); for (var i = 0; i < this.data.length[0]; i++) { if (i > 0) { string_builder.append (",\n"); } string_builder.append (" { "); for (var j = 0; j < this.data.length[1]; j++) { if (j > 0) { string_builder.append (", "); } string_builder.append ("%.3g".printf (this.data[i, j])); } string_builder.append (" }"); } string_builder.append ("\n}"); return string_builder.str; } } /** * Class for representing a variable-size 3D array. It's intended for large data, that's why * it's not a `struct`. * * API is inspired by `numpy`. */ [Compact] public class Matrix3D { private const int DIMENSIONS = 3; public uint[] shape; public double[,,] data; public Matrix3D (uint shape_0, uint shape_1, uint shape_2) { this.shape = { shape_0, shape_1, shape_2 }; this.data = new double[shape_0, shape_1, shape_2]; } public Matrix3D.from_array (double[,,] array) { this.shape = { array.length[0], array.length[1], array.length[2] }; this.data = array; } ~Matrix3D () { this.shape = null; this.data = null; } private inline bool validate_axis (ref int axis) { if (axis < 0) { axis = DIMENSIONS + axis; } return axis < DIMENSIONS; } private inline bool validate_indices (ref int i, ref int j, ref int k) { if (i < 0) { i = this.data.length[0] + i; } if (j < 0) { j = this.data.length[1] + j; } if (k < 0) { k = this.data.length[2] + k; } return i < this.data.length[0] && j < this.data.length[1] && k < this.data.length[2]; } private inline bool validate_index (ref int axis, ref int index) { if (!this.validate_axis (ref axis)) { return false; } if (index < 0) { index = (int) this.shape[axis] + index; } return index < (int) this.shape[axis]; } public void resize (uint shape_0, uint shape_1, uint shape_2, double default_value = 0.0) { var intersect_0 = uint.min (shape_0, this.shape[0]); var intersect_1 = uint.min (shape_1, this.shape[1]); var intersect_2 = uint.min (shape_2, this.shape[2]); var data = new double[shape_0, shape_1, shape_2]; if (default_value != 0.0) { for (var i = 0; i < shape_0; i++) { for (var j = 0; j < shape_1; j++) { for (var k = 0; k < shape_2; k++) { data[i, j, k] = default_value; } } } } for (var i = 0; i < intersect_0; i++) { for (var j = 0; j < intersect_1; j++) { for (var k = 0; k < intersect_2; k++) { data[i, j, k] = this.data[i, j, k]; } } } this.data = data; this.shape = { shape_0, shape_1, shape_2 }; } public double @get (int i, int j, int k, double default_value = double.NAN) { return this.validate_indices (ref i, ref j, ref k) ? this.data[i, j, k] : default_value; } public void @set (int i, int j, int k, double value) { if (this.validate_indices (ref i, ref j, ref k)) { this.data[i, j, k] = value; } else { GLib.warning ("Matrix indices %u, %u, %u are out of bounds", i, j, k); } } public void fill (double value) { for (var i = 0; i < this.shape[0]; i++) { for (var j = 0; j < this.shape[1]; j++) { for (var k = 0; k < this.shape[2]; k++) { this.data[i, j, k] = value; } } } } public bool equals (Ft.Matrix3D other) { if (this.shape[0] != other.shape[0] || this.shape[1] != other.shape[1] || this.shape[2] != other.shape[2]) { return false; } for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { for (var k = 0; k < this.data.length[2]; k++) { if ((this.data[i, j, k] - other.data[i, j, k]).abs () > double.EPSILON) { return false; } } } } return true; } public inline Ft.Matrix? get_matrix_internal (int axis, int index) { switch (axis) { case 0: var data = new double[this.data.length[1], this.data.length[2]]; for (var j = 0; j < this.data.length[1]; j++) { for (var k = 0; k < this.data.length[2]; k++) { data[j, k] = this.data[index, j, k]; } } return new Ft.Matrix.from_array (data); case 1: var data = new double[this.data.length[0], this.data.length[2]]; for (var i = 0; i < this.data.length[0]; i++) { for (var k = 0; k < this.data.length[2]; k++) { data[i, k] = this.data[i, index, k]; } } return new Ft.Matrix.from_array (data); case 2: var data = new double[this.data.length[0], this.data.length[1]]; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { data[i, j] = this.data[i, j, index]; } } return new Ft.Matrix.from_array (data); default: assert_not_reached (); } } public Ft.Matrix? get_matrix (int axis, int index) { if (!this.validate_index (ref axis, ref index)) { return null; } return this.get_matrix_internal (axis, index); } public double sum () { var result = 0.0; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { for (var k = 0; k < this.data.length[2]; k++) { result += this.data[i, j, k]; } } } return result; } public double min () { var result = this.shape[0] > 0 && this.shape[1] > 0 && this.shape[2] > 0 ? this.data[0, 0, 0] : double.NAN; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { for (var k = 0; k < this.data.length[2]; k++) { if (result > this.data[i, j, k]) { result = this.data[i, j, k]; } } } } return result; } public double max () { var result = this.shape[0] > 0 && this.shape[1] > 0 && this.shape[2] > 0 ? this.data[0, 0, 0] : double.NAN; for (var i = 0; i < this.data.length[0]; i++) { for (var j = 0; j < this.data.length[1]; j++) { for (var k = 0; k < this.data.length[2]; k++) { if (result < this.data[i, j, k]) { result = this.data[i, j, k]; } } } } return result; } /** * Split an array into multiple sub-arrays along given axis. * * For example, splitting along the last axis. You can visualise it as an array of vectors. * Splitting will replace those vectors with numeric values, and will return same number of * matrices as the vectors length. */ public Ft.Matrix[] unstack (int axis = -1) { assert (validate_axis (ref axis)); uint[] shape = { 0, 0 }; for (var index = 0; index < DIMENSIONS; index++) { if (index < axis) { shape[index] = this.shape[index]; } else if (index > axis) { shape[index - 1] = this.shape[index]; } } var result = new Ft.Matrix[this.shape[axis]]; for (var index = 0; index < this.shape[axis]; index++) { result[index] = this.get_matrix (axis, index); } return result; } } } focustimerhq-FocusTimer-8581be2/src/core/meson.build000066400000000000000000000034611520625676500225440ustar00rootroot00000000000000libft_core_sources = files( 'action.vala', 'action-list-model.vala', 'action-manager.vala', 'background-manager.vala', 'command.vala', 'context.vala', 'cycle.vala', 'database.vala', 'date-utils.vala', 'event-producer.vala', 'event.vala', 'event-bus.vala', 'expression.vala', 'expression-parser.vala', 'gap.vala', 'gap-entry.vala', 'idle-monitor.vala', 'indicator.vala', 'job-queue.vala', 'keyboard-manager.vala', 'locale.vala', 'lock-screen.vala', 'logger.vala', 'matrix.vala', 'notification.vala', 'notification-backend.vala', 'notification-manager.vala', 'priority.vala', 'provided-object.vala', 'provider.vala', 'provider-set.vala', 'scheduler.vala', 'screen-overlay-manager.vala', 'screen-saver.vala', 'session.vala', 'session-entry.vala', 'session-manager.vala', 'session-manager-action-group.vala', 'settings.vala', 'sleep-monitor.vala', 'sound-manager.vala', 'sound-player.vala', 'sounds.vala', 'state.vala', 'stats-entry.vala', 'stats-manager.vala', 'time-block.vala', 'time-block-entry.vala', 'timer.vala', 'timer-action-group.vala', 'timestamp.vala', 'timezone-entry.vala', 'timezone-history.vala', 'timezone-monitor.vala', 'utils.vala', 'variables.vala', ) libft_core = static_library( 'ft_core', libft_core_sources, dependencies: [ gio_dep, gobject_dep, gom_dep, libm_dep, peas_dep, sqlite_dep, gstreamer_dep, gstreamer_controller_dep, posix_dep, json_dep, ], include_directories: config_h_dir, ) libft_core_dep = declare_dependency( link_with: libft_core, dependencies: [ gio_dep, gobject_dep, gom_dep, peas_dep, gstreamer_dep, gstreamer_controller_dep, posix_dep, ], include_directories: [ include_directories('.'), ], ) focustimerhq-FocusTimer-8581be2/src/core/notification-backend.vala000066400000000000000000000202431520625676500253170ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public interface NotificationBackendProvider : Ft.Provider { public abstract async void send_notification (string id, Ft.Notification notification); public abstract async void withdraw_notification (string id); } private class FallbackNotificationBackendProvider : Ft.Provider, Ft.NotificationBackendProvider { private GLib.Application? application = null; private static GLib.NotificationPriority transform_priority (Ft.NotificationPriority priority) { switch (priority) { case Ft.NotificationPriority.LOW: return GLib.NotificationPriority.LOW; case Ft.NotificationPriority.NORMAL: return GLib.NotificationPriority.NORMAL; case Ft.NotificationPriority.HIGH: return GLib.NotificationPriority.HIGH; case Ft.NotificationPriority.URGENT: return GLib.NotificationPriority.URGENT; default: assert_not_reached (); } } public async void send_notification (string id, Ft.Notification notification) { var glib_notification = new GLib.Notification (notification.title); glib_notification.set_body (notification.body); glib_notification.set_priority (transform_priority (notification.priority)); if (notification.category != null) { glib_notification.set_category (notification.category); } if (notification.icon != null) { glib_notification.set_icon (notification.icon); } if (notification.default_action != null) { glib_notification.set_default_action_and_target_value ( notification.default_action, notification.default_target_value); } notification.foreach_button ( (label, action, target_value) => { glib_notification.add_button_with_target_value (label, action, target_value); }); this.application?.send_notification (id, glib_notification); } public async void withdraw_notification (string id) { this.application?.withdraw_notification (id); } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.available = true; } public override async void uninitialize () throws GLib.Error { } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.application = GLib.Application.get_default (); } public override async void disable () throws GLib.Error { this.application = null; } } [Compact] private class QueuedNotification { public string id; public Ft.Notification? notification; public ulong serial; public QueuedNotification (string id, Ft.Notification? notification, ulong serial) { this.id = id; this.notification = notification; this.serial = serial; } ~QueuedNotification () { this.notification = null; } } public interface NotificationBackendInterface : GLib.Object { public abstract void send_notification (string id, Ft.Notification notification); public abstract void withdraw_notification (string id); } /** * `Application.send_notification()` already supports multiple backends, which works fine on * GNOME, but not so much on other desktops - especially when using "portal" backend. * On top of it, we would like some lower-level access to suppress notification sounds and to * handle rate limits. That's why a custom backend is needed in our case. */ [SingleInstance] public class NotificationBackend : Ft.ProvidedObject, Ft.NotificationBackendInterface { private GLib.Queue queue; private bool processing_queue = false; private ulong next_serial = 1U; construct { this.queue = new GLib.Queue (); } private void process_queue () { var provider = this.provider; if (provider == null || !provider.enabled || this.processing_queue) { return; } var item = this.queue.pop_head (); if (item != null) { this.processing_queue = true; if (item.notification != null) { provider.send_notification.begin ( item.id, item.notification, (obj, res) => { provider.send_notification.end (res); this.processing_queue = false; this.process_queue (); }); } else { provider.withdraw_notification.begin ( item.id, (obj, res) => { provider.withdraw_notification.end (res); this.processing_queue = false; this.process_queue (); }); } } } private void remove_from_queue (string id) { unowned var link = this.queue.head; while (link != null) { if (link.data.id == id) { unowned var next_link = link.next; this.queue.delete_link (link); link = next_link; } else { link = link.next; } } } protected override void initialize () { } protected override void setup_providers () { this.providers.add (new Ft.FallbackNotificationBackendProvider (), Ft.Priority.LOW); } protected override void provider_enabled (Ft.NotificationBackendProvider provider) { this.process_queue (); } protected override void provider_disabled (Ft.NotificationBackendProvider provider) { } public void send_notification (string id, Ft.Notification notification) { this.remove_from_queue (id); this.queue.push_tail (new QueuedNotification (id, notification, this.next_serial++)); this.process_queue (); } public void withdraw_notification (string id) { this.remove_from_queue (id); this.queue.push_tail (new QueuedNotification (id, null, this.next_serial++)); // Prioritise withdrawals over sending new notifications this.queue.sort ( (a, b) => { if (a.notification == null && b.notification != null) { return -1; } if (a.notification != null && b.notification == null) { return 1; } return a.serial < b.serial ? -1 : 1; }); this.process_queue (); } public override void dispose () { if (this.queue != null) { this.queue.clear (); this.queue = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/notification-manager.vala000066400000000000000000000773571520625676500253640ustar00rootroot00000000000000/* * Copyright (c) 2023-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { private enum NotificationType { NULL, TIME_BLOCK_ABOUT_TO_END, TIME_BLOCK_ENDED, TIME_BLOCK_STARTED, TIME_BLOCK_RUNNING, CONFIRM_ADVANCEMENT } /** * `NotificationManager` manages notification popups and the screen overlay. */ [SingleInstance] public class NotificationManager : GLib.Object { /** * Notifications may contain remaining time. As we can't update it gracefully the most sensible way is to * dismiss the notification when it's getting stale. The timeout is higher to allow urgent notifications be * shown. Unfortunately, we may dismiss the notification before it gets acknowledged. */ private const uint WITHDRAW_TIMEOUT_SECONDS = 30; private const uint SCREEN_OVERLAY_OPEN_TIMEOUT = 1000; // milliseconds private const int EXPIRY_TIMEOUT = 5000; // milliseconds private const int64 TIME_BLOCK_ABOUT_TO_END_TIMEOUT = 10 * Ft.Interval.SECOND; private const int64 TIME_BLOCK_ABOUT_TO_END_TOLERANCE = 5 * Ft.Interval.SECOND; public Ft.Timer timer { get { return this._timer; } construct { this._timer = value; this.previous_timer_state = this._timer.state.copy (); this.timer_state_changed_id = this._timer.state_changed.connect ( this.on_timer_state_changed); } } public Ft.SessionManager session_manager { get { return this._session_manager; } construct { this._session_manager = value; this.session_manager_confirm_advancement_id = this._session_manager.confirm_advancement.connect ( this.on_session_manager_confirm_advancement); } } public Ft.NotificationBackendInterface backend { get { return this._backend; } construct { this._backend = value; } } private Ft.Timer? _timer = null; private Ft.SessionManager? _session_manager = null; private Ft.NotificationBackendInterface? _backend = null; private GLib.Settings? settings = null; private Ft.IdleMonitor? idle_monitor = null; private Ft.LockScreen? lock_screen = null; private Ft.TimerState previous_timer_state; private ulong timer_state_changed_id = 0; private ulong timer_tick_id = 0; private ulong settings_changed_id = 0; private ulong session_manager_confirm_advancement_id = 0; private int inhibit_count = 0; private bool screen_overlay_active = false; private uint screen_overlay_open_timeout_id = 0U; private uint update_timeout_id = 0U; private uint withdraw_timeout_id = 0U; private uint lock_screen_idle_id = 0U; private uint reopen_screen_overlay_idle_id = 0U; private Ft.Notification? notification = null; private Ft.NotificationType notification_type = NotificationType.NULL; private weak Ft.TimeBlock? notification_time_block = null; construct { this.settings = Ft.get_settings (); this.idle_monitor = new Ft.IdleMonitor (); this.lock_screen = new Ft.LockScreen (); this.settings_changed_id = this.settings.changed.connect (this.on_settings_changed); this.schedule_announcements (); if (this.notification == null) { this._backend.withdraw_notification ("timer"); } } public NotificationManager () { GLib.Object ( timer: Ft.Timer.get_default (), session_manager: Ft.SessionManager.get_default (), backend: new Ft.NotificationBackend () ); } public NotificationManager.with_backend (Ft.NotificationBackendInterface backend) { GLib.Object ( timer: Ft.Timer.get_default (), session_manager: Ft.SessionManager.get_default (), backend: backend ); } private string format_remaining_time (Ft.TimeBlock time_block) { var timestamp = this._timer.get_last_tick_time (); var seconds = Ft.Timestamp.to_seconds (time_block.calculate_remaining (timestamp)); var seconds_uint = (uint) Ft.round_seconds (seconds); // translators: time remaining eg. "3 minutes 50 seconds remaining" return _("%s remaining").printf (Ft.format_time (seconds_uint)); } /** * Check basic conditions if the screen overlay is possible for the current timer state. */ private bool can_open_screen_overlay_later () { if (!this._timer.is_running ()) { return false; } var current_time_block = this._session_manager.current_time_block; if (current_time_block == null || !current_time_block.state.is_break ()) { return false; } if (!this.settings.get_boolean ("screen-overlay")) { return false; } return true; } /** * Return whether screen overlay can be opened right now. */ private bool can_open_screen_overlay () { if (!this.can_open_screen_overlay_later ()) { return false; } // Don't interrupt the current announcement. if (this.notification_type == Ft.NotificationType.TIME_BLOCK_ABOUT_TO_END && this.notification_time_block != null && this.notification_time_block.state.is_break ()) { return false; } // TODO: check if we're not interrupting a drag-and-drop or a videocall return true; } private void on_lock_screen_idle () { if (!this.screen_overlay_active) { return; } this.lock_screen.activate (); } private void on_reopen_screen_overlay_idle () { if (this.screen_overlay_active) { GLib.debug ("Screen overlay has already opened."); return; } if (!this.can_open_screen_overlay ()) { GLib.debug ("Screen overlay not allowed."); return; } // Don't reopen if close to announcement notification. var timestamp = this._timer.get_current_time (); var about_to_end_threshold = this.get_about_to_end_duration () + TIME_BLOCK_ABOUT_TO_END_TOLERANCE; if (this._timer.calculate_remaining (timestamp) <= about_to_end_threshold) { return; } // Request the overlay this.emit_request_screen_overlay_open (); } private void add_lock_screen_idle_watch () { var lock_delay = Ft.Timestamp.from_milliseconds_uint ( this.settings.get_uint ("screen-overlay-lock-delay") * 1000U); if (this.lock_screen_idle_id == 0 && lock_delay > 0 && this.idle_monitor.enabled) { this.lock_screen_idle_id = this.idle_monitor.add_idle_watch (lock_delay, true, this.on_lock_screen_idle, GLib.get_monotonic_time ()); } } private void remove_lock_screen_idle_watch () { if (this.lock_screen_idle_id != 0) { this.idle_monitor.remove_watch (this.lock_screen_idle_id); this.lock_screen_idle_id = 0; } } private void add_reopen_screen_overlay_idle_watch () { if (this.reopen_screen_overlay_idle_id != 0) { return; } if (!this.idle_monitor.enabled || !this.can_open_screen_overlay_later ()) { return; } var reopen_delay = Ft.Timestamp.from_milliseconds_uint ( this.settings.get_uint ("screen-overlay-reopen-delay") * 1000U); this.reopen_screen_overlay_idle_id = this.idle_monitor.add_idle_watch ( reopen_delay, false, this.on_reopen_screen_overlay_idle, GLib.get_monotonic_time ()); } private void remove_reopen_screen_overlay_idle_watch () { if (this.reopen_screen_overlay_idle_id != 0) { this.idle_monitor.remove_watch (this.reopen_screen_overlay_idle_id); this.reopen_screen_overlay_idle_id = 0; } } private void reset_reopen_screen_overlay_idle_watch () { this.remove_reopen_screen_overlay_idle_watch (); this.add_reopen_screen_overlay_idle_watch (); } private void remove_withdraw_timeout () { if (this.withdraw_timeout_id != 0U) { GLib.Source.remove (this.withdraw_timeout_id); this.withdraw_timeout_id = 0U; } } private void withdraw_notifications () { this.remove_withdraw_timeout (); if (this.notification == null) { return; } this._backend.withdraw_notification ("timer"); this.notification = null; this.notification_type = Ft.NotificationType.NULL; this.notification_time_block = null; } private void schedule_withdraw_notifications () { this.remove_withdraw_timeout (); // TODO: ensure user is active / acknowledged notification this.withdraw_timeout_id = GLib.Timeout.add_seconds ( WITHDRAW_TIMEOUT_SECONDS, () => { this.withdraw_timeout_id = 0U; this.withdraw_notifications (); return GLib.Source.REMOVE; } ); GLib.Source.set_name_by_id (this.withdraw_timeout_id, "Ft.NotificationManager.schedule_withdraw_notifications"); } private Ft.Notification create_notification (string title, string body, bool activate_screen_overlay = false) { var notification = new Ft.Notification (title, body); notification.priority = Ft.NotificationPriority.HIGH; notification.is_transient = true; notification.suppress_sound = true; if (activate_screen_overlay) { notification.set_default_action ("app.screen-overlay"); } else { notification.set_default_action_and_target_value ("app.window", new GLib.Variant.string ("timer")); } return notification; } /** * Show notification informing that the time-block has started. */ private void notify_time_block_started (Ft.TimeBlock time_block) { var title = ""; var body = this.format_remaining_time (time_block); switch (time_block.state) { case Ft.State.POMODORO: title = _("Pomodoro"); break; case Ft.State.BREAK: title = _("Take a break"); break; case Ft.State.SHORT_BREAK: title = _("Take a short break"); break; case Ft.State.LONG_BREAK: title = _("Take a long break"); break; default: assert_not_reached (); } var notification = this.create_notification ( title, body, time_block.state.is_break ()); notification.event_id = "time-block-started"; notification.expire_timeout = EXPIRY_TIMEOUT; this.notification = notification; this.notification_type = Ft.NotificationType.TIME_BLOCK_STARTED; this.notification_time_block = time_block; this._backend.send_notification ("timer", this.notification); this.schedule_withdraw_notifications (); } /** * Show notification with current state and remaining time. */ private void notify_time_block_running (Ft.TimeBlock time_block) { var title = time_block.state.get_label (); var body = this.format_remaining_time (time_block); var notification = this.create_notification ( title, body, time_block.state.is_break ()); notification.event_id = "time-block-running"; notification.expire_timeout = EXPIRY_TIMEOUT; this.notification = notification; this.notification_type = Ft.NotificationType.TIME_BLOCK_RUNNING; this.notification_time_block = time_block; this._backend.send_notification ("timer", this.notification); this.schedule_withdraw_notifications (); } /** * Show notification informing that the time-block has ended. */ private void notify_time_block_about_to_end (Ft.TimeBlock time_block) { var title = ""; var body = ""; var action_label = ""; switch (time_block.state) { case Ft.State.POMODORO: title = _("Pomodoro is about to end"); action_label = _("Take a Break"); break; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: title = _("Break is about to end"); action_label = _("Start Pomodoro"); break; default: assert_not_reached (); } var notification = this.create_notification (title, body, false); notification.priority = Ft.NotificationPriority.URGENT; notification.event_id = "time-block-about-to-end"; notification.add_button_with_target_value (_("+1 minute"), "app.extend", new GLib.Variant.int32 (60)); notification.add_button (action_label, "app.advance"); this.notification = notification; this.notification_type = Ft.NotificationType.TIME_BLOCK_ABOUT_TO_END; this.notification_time_block = time_block; this._backend.send_notification ("timer", this.notification); this.remove_withdraw_timeout (); } /** * Show notification informing that the time-block has ended. * * It's only shown when waiting for activity. */ private void notify_time_block_ended (Ft.TimeBlock previous_time_block) { var title = ""; var body = _("Get ready…"); switch (previous_time_block.state) { case Ft.State.POMODORO: title = _("Pomodoro is over!"); break; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: title = _("Break is over!"); break; default: assert_not_reached (); } var notification = this.create_notification (title, body, false); notification.priority = Ft.NotificationPriority.URGENT; notification.event_id = "time-block-ended"; this.notification = notification; this.notification_type = Ft.NotificationType.TIME_BLOCK_ENDED; this.notification_time_block = previous_time_block; this._backend.send_notification ("timer", this.notification); this.remove_withdraw_timeout (); } /** * Show notification with emphasis on confirming advancement to the next time-block. */ private void notify_confirm_advancement (Ft.TimeBlock current_time_block, Ft.TimeBlock next_time_block) { var title = ""; var body = ""; var action_label = ""; switch (current_time_block.state) { case Ft.State.POMODORO: title = _("Pomodoro is over!"); break; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: title = _("Break is over!"); break; default: assert_not_reached (); } switch (next_time_block.state) { case Ft.State.POMODORO: body = _("Confirm the start of a Pomodoro…"); action_label = _("Start Pomodoro"); break; case Ft.State.BREAK: body = _("Confirm the start of a break…"); action_label = _("Take a Break"); break; case Ft.State.SHORT_BREAK: body = _("Confirm the start of a short break…"); action_label = _("Take a Break"); break; case Ft.State.LONG_BREAK: body = _("Confirm the start of a long break…"); action_label = _("Take a Break"); break; default: assert_not_reached (); } var notification = this.create_notification (title, body, false); notification.priority = Ft.NotificationPriority.URGENT; notification.event_id = "confirm-advancement"; if (next_time_block.state.is_break ()) { notification.add_button_with_target_value (_("Skip Break"), "app.advance-to-state", new GLib.Variant.string ("pomodoro")); } notification.add_button (action_label, "app.advance"); this.notification = notification; this.notification_type = Ft.NotificationType.CONFIRM_ADVANCEMENT; this.notification_time_block = current_time_block; this._backend.send_notification ("timer", this.notification); this.remove_withdraw_timeout (); } private int64 get_about_to_end_duration () { if (!this.settings.get_boolean ("announce-about-to-end")) { return 0; } return TIME_BLOCK_ABOUT_TO_END_TIMEOUT; } private void update_full (Ft.TimerState current_state, Ft.TimerState previous_state, bool allow_screen_overlay) { if (this.session_manager.current_session == null) { this.withdraw_notifications (); return; } if (this.update_timeout_id != 0U) { GLib.Source.remove (this.update_timeout_id); this.update_timeout_id = 0U; } var current_time_block = current_state.user_data as Ft.TimeBlock; var previous_time_block = previous_state.user_data as Ft.TimeBlock; var timestamp = this._timer.get_current_time (); this.previous_timer_state = previous_state.copy (); if (!this.can_open_screen_overlay_later ()) { this.emit_request_screen_overlay_close (true); this.remove_reopen_screen_overlay_idle_watch (); } if (current_state.is_paused () || current_time_block == null) { this.withdraw_notifications (); } else if (current_state.is_finished ()) { // Either SessionManager will advance to the next time-block or // `confirm_advancement` signal will be emitted. So, nothing to do here. } else if (!current_state.is_started ()) { if (previous_time_block != null) { this.notify_time_block_ended (previous_time_block); } } else if (current_state.is_running ()) { var remaining = this._timer.calculate_remaining (timestamp); var about_to_end_duration = this.get_about_to_end_duration () + TIME_BLOCK_ABOUT_TO_END_TOLERANCE; var is_rewinding = current_state.user_data == previous_state.user_data && current_state.paused_time == previous_state.paused_time && current_state.offset != previous_state.offset; if (current_time_block.state.is_break ()) { if (remaining >= about_to_end_duration && !is_rewinding && allow_screen_overlay && this.can_open_screen_overlay ()) { this.emit_request_screen_overlay_open (); return; } } this.add_reopen_screen_overlay_idle_watch (); if (current_state.started_time == timestamp) { this.notify_time_block_started (current_time_block); } else if (remaining < about_to_end_duration) { this.notify_time_block_about_to_end (current_time_block); } else { this.notify_time_block_running (current_time_block); } } else { assert_not_reached (); } } private void update (bool allow_screen_overlay) { this.update_full (this._timer.state, this.previous_timer_state, allow_screen_overlay); } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update_full (current_state, previous_state, true); } private void on_timer_tick (int64 timestamp) { if (this.screen_overlay_active) { return; } if (this.notification_type == Ft.NotificationType.TIME_BLOCK_ABOUT_TO_END || this.notification_type == Ft.NotificationType.TIME_BLOCK_ENDED || this.notification_type == Ft.NotificationType.CONFIRM_ADVANCEMENT) { return; } var remaining = this._timer.calculate_remaining (timestamp); if (remaining <= this.get_about_to_end_duration () && remaining >= TIME_BLOCK_ABOUT_TO_END_TOLERANCE) { this.notify_time_block_about_to_end (this.session_manager.current_time_block); } } private void on_session_manager_confirm_advancement (Ft.TimeBlock current_time_block, Ft.TimeBlock next_time_block) { this.emit_request_screen_overlay_close (); this.notify_confirm_advancement (current_time_block, next_time_block); } private void on_settings_changed (GLib.Settings settings, string key) { if (this.inhibit_count > 0) { return; } switch (key) { case "announce-about-to-end": this.schedule_announcements (); break; case "screen-overlay": case "screen-overlay-reopen-delay": if (this.reopen_screen_overlay_idle_id != 0) { this.reset_reopen_screen_overlay_idle_watch (); } break; } } private void schedule_announcements () { if (this.settings.get_boolean ("announce-about-to-end")) { if (this.timer_tick_id == 0) { this.timer_tick_id = this._timer.tick.connect (this.on_timer_tick); } } else { if (this.timer_tick_id != 0) { this._timer.disconnect (this.timer_tick_id); this.timer_tick_id = 0; } } } private void emit_request_screen_overlay_open () { if (this.screen_overlay_active || this.screen_overlay_open_timeout_id != 0U) { return; } this.screen_overlay_open_timeout_id = GLib.Timeout.add ( SCREEN_OVERLAY_OPEN_TIMEOUT, () => { // Open notification as a fallback this.screen_overlay_open_timeout_id = 0; this.remove_lock_screen_idle_watch (); this.update (false); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.screen_overlay_open_timeout_id, "Ft.NotificationManager.emit_request_screen_overlay_open"); this.request_screen_overlay_open (); } private void emit_request_screen_overlay_close (bool force_close = false) { if (!force_close && !this.screen_overlay_active && this.screen_overlay_open_timeout_id == 0U) { return; } if (this.screen_overlay_open_timeout_id != 0U) { GLib.Source.remove (this.screen_overlay_open_timeout_id); this.screen_overlay_open_timeout_id = 0U; } if (this.screen_overlay_active) { this.request_screen_overlay_close (); } if (force_close && this.screen_overlay_active) { this.screen_overlay_closed (); } } public void inhibit () { this.inhibit_count++; if (this.inhibit_count == 1) { this.withdraw_notifications (); this.remove_reopen_screen_overlay_idle_watch (); this.remove_lock_screen_idle_watch (); GLib.SignalHandler.block (this._timer, this.timer_state_changed_id); GLib.SignalHandler.block (this._session_manager, this.session_manager_confirm_advancement_id); if (this.timer_tick_id != 0) { this._timer.disconnect (this.timer_tick_id); this.timer_tick_id = 0; } } } public void uninhibit (bool notify = true) { this.inhibit_count--; if (this.inhibit_count == 0) { GLib.SignalHandler.unblock (this._timer, this.timer_state_changed_id); GLib.SignalHandler.unblock (this._session_manager, this.session_manager_confirm_advancement_id); this.schedule_announcements(); if (notify) { this.update (false); } } } public void emit_screen_overlay_opened () { this.screen_overlay_opened (); } public void emit_screen_overlay_closed () { this.screen_overlay_closed (); } /** * Notify manager that screen overlay has opened. */ public signal void screen_overlay_opened () { if (this.screen_overlay_open_timeout_id != 0U) { GLib.Source.remove (this.screen_overlay_open_timeout_id); this.screen_overlay_open_timeout_id = 0U; } this.screen_overlay_active = true; this.remove_reopen_screen_overlay_idle_watch (); this.add_lock_screen_idle_watch (); this.withdraw_notifications (); } /** * Notify manager that screen overlay has closed. */ public signal void screen_overlay_closed () { this.screen_overlay_active = false; this.remove_lock_screen_idle_watch (); // Let the notification server acknowledge there is no full-screen window, // otherwise notification could get blocked. this.update_timeout_id = GLib.Timeout.add (100, () => { this.update_timeout_id = 0; this.update (false); return GLib.Source.REMOVE; }); } public signal void request_screen_overlay_open (); public signal void request_screen_overlay_close (); public void destroy () { this.withdraw_notifications (); if (this.update_timeout_id != 0U) { GLib.Source.remove (this.update_timeout_id); this.update_timeout_id = 0U; } if (this.screen_overlay_open_timeout_id != 0U) { GLib.Source.remove (this.screen_overlay_open_timeout_id); this.screen_overlay_open_timeout_id = 0U; } } public override void dispose () { this.destroy (); if (this.screen_overlay_active) { this.request_screen_overlay_close (); } if (this.timer_tick_id != 0) { this._timer.disconnect (this.timer_tick_id); this.timer_tick_id = 0; } if (this.timer_state_changed_id != 0) { this._timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } if (this.session_manager_confirm_advancement_id != 0) { this._session_manager.disconnect (this.session_manager_confirm_advancement_id); this.session_manager_confirm_advancement_id = 0; } if (this.settings_changed_id != 0) { this.settings.disconnect (this.settings_changed_id); this.settings_changed_id = 0; } this.remove_reopen_screen_overlay_idle_watch (); this.remove_lock_screen_idle_watch (); this._timer = null; this._session_manager = null; this._backend = null; this.settings = null; this.notification = null; this.notification_time_block = null; this.idle_monitor = null; this.lock_screen = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/notification.vala000066400000000000000000000115451520625676500237370ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { private struct NotificationButton { public string label; public string action; public GLib.Variant? target_value; } public enum NotificationPriority { LOW, NORMAL, HIGH, URGENT; public string to_string () { switch (this) { case LOW: return "low"; case NORMAL: return "normal"; case HIGH: return "high"; case URGENT: return "urgent"; default: assert_not_reached (); } } } public delegate void NotificationForeachButtonFunc (string label, string action, GLib.Variant? target_value); /** * `GLib.Notification` equivalent, but fully editable and with extra fields. */ public class Notification { public string title; public string body; public string? category = null; public string? event_id = null; public GLib.Icon? icon = null; public bool is_transient = false; public bool suppress_sound = false; public Ft.NotificationPriority priority = Ft.NotificationPriority.NORMAL; public string? default_action = null; public GLib.Variant? default_target_value = null; public int expire_timeout = -1; private Ft.NotificationButton[] buttons; public Notification (string title, string body) { this.title = title; this.body = body; this.buttons = {}; } ~Notification () { this.category = null; this.event_id = null; this.icon = null; this.default_action = null; this.default_target_value = null; this.buttons = null; } public void set_default_action_and_target_value (string action, GLib.Variant? target_value) { this.default_action = action; this.default_target_value = target_value; } public void set_default_action (string detailed_action) { string action; GLib.Variant? target_value; try { GLib.Action.parse_detailed_name (detailed_action, out action, out target_value); this.set_default_action_and_target_value (action, target_value); } catch (GLib.Error error) { GLib.warning ("Failed to set notification default action: %s", error.message); } } public void add_button_with_target_value (string label, string action, GLib.Variant? target_value) { this.buttons += Ft.NotificationButton () { label = label, action = action, target_value = target_value, }; } public void add_button (string label, string detailed_action) { string action; GLib.Variant? target_value; try { GLib.Action.parse_detailed_name (detailed_action, out action, out target_value); this.add_button_with_target_value (label, action, target_value); } catch (GLib.Error error) { GLib.warning ("Failed to add notification button: %s", error.message); } } public void foreach_button (Ft.NotificationForeachButtonFunc func) { foreach (var button in this.buttons) { func (button.label, button.action, button.target_value); } } /** * Compare visible fields and tell whether notifications are roughly the same. */ public bool is_similar (Ft.Notification other) { if (this.title != other.title) { return false; } if (this.body != other.body) { return false; } return true; } } } focustimerhq-FocusTimer-8581be2/src/core/priority.vala000066400000000000000000000017331520625676500231300ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public enum Priority { LOW = 0, DEFAULT = 1, HIGH = 2; public string to_string () { switch (this) { case LOW: return "low"; case DEFAULT: return "default"; case HIGH: return "high"; default: assert_not_reached (); } } public static Ft.Priority from_string (string? priority) { switch (priority) { case "low": return LOW; case "default": return DEFAULT; case "high": return HIGH; default: return DEFAULT; } } } } focustimerhq-FocusTimer-8581be2/src/core/provided-object.vala000066400000000000000000000126061520625676500243300ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public abstract class ProvidedObject : GLib.Object { [CCode (notify = false)] public bool available { get { return _available; } private set { if (this._available != value) { this._available = value; this.notify_property ("available"); } } } [CCode (notify = false)] public bool enabled { get { return _enabled; } private set { if (this._enabled != value) { this._enabled = value; this.notify_property ("enabled"); } } } /** * A selected provider. It may not be available nor enabled. */ [CCode (notify = false)] public unowned T provider { get { return (T) this._provider; } private set { var provider = value as Ft.Provider; if (this._provider != provider) { this._provider = provider; this.notify_property ("provider"); this.available = provider != null ? provider.available : false; this.enabled = provider != null ? provider.enabled : false; } } } protected Ft.ProviderSet providers = null; private Ft.Provider _provider = null; private bool _available = false; private bool _enabled = false; private ulong provider_selected_id = 0; private ulong provider_unselected_id = 0; private ulong provider_enabled_id = 0; private ulong provider_disabled_id = 0; construct { this.providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); this.provider_selected_id = this.providers.provider_selected.connect (this.on_provider_selected); this.provider_unselected_id = this.providers.provider_unselected.connect (this.on_provider_unselected); this.provider_enabled_id = this.providers.provider_enabled.connect (this.on_provider_enabled); this.provider_disabled_id = this.providers.provider_disabled.connect (this.on_provider_disabled); this.initialize (); this.setup_providers (); this.providers.discover (); this.providers.enable (); } private void on_provider_notify_available (GLib.Object object, GLib.ParamSpec pspec) { var provider = (Ft.Provider) object; if (this._provider == provider) { this.available = provider.available; } } private void on_provider_selected (T provider) { var _provider = (Ft.Provider) provider; _provider.notify["available"].connect (this.on_provider_notify_available); this.provider = _provider; } private void on_provider_unselected (T provider) { var _provider = (Ft.Provider) provider; _provider.notify["available"].disconnect (this.on_provider_notify_available); if (this._provider == _provider) { this.provider = null; } } private void on_provider_enabled (T provider) { var _provider = (Ft.Provider) provider; if (this._provider == _provider) { this.enabled = true; } this.provider_enabled (provider); } private void on_provider_disabled (T provider) { var _provider = (Ft.Provider) provider; if (this._provider == _provider) { this.enabled = false; } this.provider_disabled (provider); } protected abstract void initialize (); protected abstract void setup_providers (); protected abstract void provider_enabled (T provider); protected abstract void provider_disabled (T provider); public override void dispose () { if (this._provider != null) { this._provider.notify["available"].disconnect (this.on_provider_notify_available); } if (this.provider_selected_id != 0) { this.providers.disconnect (this.provider_selected_id); this.provider_selected_id = 0; } if (this.provider_unselected_id != 0) { this.providers.disconnect (this.provider_unselected_id); this.provider_unselected_id = 0; } if (this.provider_enabled_id != 0) { this.providers.disconnect (this.provider_enabled_id); this.provider_enabled_id = 0; } if (this.provider_disabled_id != 0) { this.providers.disconnect (this.provider_disabled_id); this.provider_disabled_id = 0; } this._provider = null; this.providers = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/provider-set.vala000066400000000000000000000752001520625676500236720ustar00rootroot00000000000000/* * Copyright (c) 2024-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * Time between the start of provider initialization to resolution of availability. */ private const int64 AVAILABILITY_TIMEOUT = Ft.Interval.MILLISECOND * 100; private const int64 AVAILABILITY_TIMEOUT_TOLERANCE = Ft.Interval.MILLISECOND * 20; // XXX: remove - only SINGLE is used public enum SelectionMode { NONE, SINGLE, ALL } private enum ProviderStatus { NOT_INITIALIZED, INITIALIZING, UNINITIALIZING, DISABLING, DISABLED, ENABLING, ENABLED; public bool is_transient () { switch (this) { case INITIALIZING: case UNINITIALIZING: case DISABLING: case ENABLING: return true; default: return false; } } } private enum ProviderAvailability { UNKNOWN, AVAILABLE, UNAVAILABLE; public static int compare (ProviderAvailability value, ProviderAvailability other) { if (value == other) { return 0; } if (value == ProviderAvailability.AVAILABLE) { return -1; } if (other == ProviderAvailability.AVAILABLE) { return 1; } return value == ProviderAvailability.UNKNOWN ? -1 : 1; } } private class ProviderInfo { public Ft.Provider instance; public Ft.Priority priority; public Ft.ProviderStatus status = Ft.ProviderStatus.NOT_INITIALIZED; public bool selected = false; public GLib.Cancellable? cancellable = null; public int64 initialization_time = Ft.Timestamp.UNDEFINED; public bool destroying = false; public ProviderInfo (Ft.Provider instance, Ft.Priority priority) { this.instance = instance; this.priority = priority; } ~ProviderInfo () { // Expect to use .destroy() until provider gets unintialized assert (this.instance == null); } public int64 get_availability_timeout (ref int64 monotonic_time) { if (this.instance.available_set) { return 0; } if (Ft.Timestamp.is_undefined (this.initialization_time)) { return 0; } if (Ft.Timestamp.is_undefined (monotonic_time)) { monotonic_time = GLib.get_monotonic_time (); } return monotonic_time - this.initialization_time; } private void destroy_internal () { var provider = this.instance; if (provider == null || this.status.is_transient ()) { return; } if (this.status == Ft.ProviderStatus.ENABLED) { this.status = Ft.ProviderStatus.DISABLING; provider.enabled = false; provider.disable.begin ( (obj, res) => { try { provider.disable.end (res); } catch (GLib.Error error) { GLib.warning ("Error while disabling %s: %s", provider.get_type ().name (), error.message); } this.status = Ft.ProviderStatus.DISABLED; this.destroy_internal (); }); } else if (this.status == Ft.ProviderStatus.DISABLED) { this.status = Ft.ProviderStatus.UNINITIALIZING; provider.uninitialize.begin ( (obj, res) => { try { provider.uninitialize.end (res); } catch (GLib.Error error) { GLib.warning ("Error while uninitializing %s: %s", provider.get_type ().name (), error.message); } this.status = Ft.ProviderStatus.NOT_INITIALIZED; this.destroy_internal (); }); } else if (this.status == Ft.ProviderStatus.NOT_INITIALIZED) { this.instance = null; } } public void destroy () { this.destroying = true; if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } this.destroy_internal (); } } public class ProviderSet : GLib.Object { public Ft.SelectionMode selection_mode { get { return this._selection_mode; } construct { this._selection_mode = value; } } private Ft.SelectionMode _selection_mode = Ft.SelectionMode.ALL; private GLib.GenericSet providers = null; private Peas.ExtensionSet? extension_set = null; private uint update_selection_timeout_id = 0; private uint update_selection_idle_id = 0; private bool selection_invalid = false; private bool updating_selection = false; private bool should_enable = false; construct { this.providers = new GLib.GenericSet (GLib.direct_hash, GLib.direct_equal); } public ProviderSet (Ft.SelectionMode selection_mode = Ft.SelectionMode.ALL) { GLib.Object ( selection_mode: selection_mode ); } /** * Manage provider according to its status. * * It should be called after every async action or status changed. */ private void check_provider_status (Ft.ProviderInfo provider_info) { var provider = provider_info.instance; if (provider == null || provider_info.destroying) { return; } // Each action should call check_provider_status() at the end, so if the status is transient // we can ignore it. if (provider_info.status.is_transient ()) { return; } if (provider_info.selected) { if (provider_info.status == Ft.ProviderStatus.NOT_INITIALIZED) { provider_info.status = Ft.ProviderStatus.INITIALIZING; provider_info.cancellable = new GLib.Cancellable (); provider_info.initialization_time = GLib.get_monotonic_time (); provider.initialize.begin ( provider_info.cancellable, (obj, res) => { try { provider.initialize.end (res); provider_info.status = Ft.ProviderStatus.DISABLED; this.check_provider_status (provider_info); if (provider_info.selected && !provider.available_set) { this.queue_update_selection (); } } catch (GLib.Error error) { GLib.warning ("Error while initializing %s: %s", provider.get_type ().name (), error.message); provider_info.status = Ft.ProviderStatus.NOT_INITIALIZED; } }); } else if (this.should_enable && provider_info.status == Ft.ProviderStatus.DISABLED && provider.available) { provider_info.status = Ft.ProviderStatus.ENABLING; provider_info.cancellable = new GLib.Cancellable (); provider.enable.begin ( provider_info.cancellable, (obj, res) => { try { provider.enable.end (res); provider_info.status = Ft.ProviderStatus.ENABLED; provider.enabled = true; this.check_provider_status (provider_info); } catch (GLib.Error error) { GLib.warning ("Error while enabling %s: %s", provider.get_type ().name (), error.message); provider_info.status = Ft.ProviderStatus.DISABLED; if (provider_info.destroying) { provider_info.destroy (); } } }); } else if (!this.should_enable && provider_info.status == Ft.ProviderStatus.ENABLED) { provider_info.status = Ft.ProviderStatus.DISABLING; provider.enabled = false; provider.disable.begin ( (obj, res) => { try { provider.disable.end (res); provider_info.status = Ft.ProviderStatus.DISABLED; this.check_provider_status (provider_info); } catch (GLib.Error error) { GLib.warning ("Error while disabling %s: %s", provider.get_type ().name (), error.message); provider_info.status = Ft.ProviderStatus.DISABLED; if (provider_info.destroying) { provider_info.destroy (); } } }); } } else if (provider_info.status == Ft.ProviderStatus.ENABLED) { // Disable unselected providers. We try to disable even if unavailable. provider_info.status = Ft.ProviderStatus.DISABLING; provider.enabled = false; provider.disable.begin ( (obj, res) => { try { provider.disable.end (res); } catch (GLib.Error error) { GLib.warning ("Error while disabling %s: %s", provider.get_type ().name (), error.message); } provider_info.status = Ft.ProviderStatus.DISABLED; this.check_provider_status (provider_info); }); } } private static ProviderAvailability get_availability (Ft.ProviderInfo provider_info, int64 provider_timeout) { if (provider_info.instance.available_set) { return provider_info.instance.available ? ProviderAvailability.AVAILABLE : ProviderAvailability.UNAVAILABLE; } if (provider_info.status == Ft.ProviderStatus.NOT_INITIALIZED || provider_info.status == Ft.ProviderStatus.INITIALIZING || provider_timeout < AVAILABILITY_TIMEOUT) { return ProviderAvailability.UNKNOWN; } return ProviderAvailability.UNAVAILABLE; } private static int compare (Ft.ProviderInfo provider_info, int64 provider_timeout, Ft.ProviderInfo other_info, int64 other_timeout) { var provider_availability = get_availability (provider_info, provider_timeout); var other_availability = get_availability (other_info, other_timeout); if (provider_availability != other_availability) { if (provider_availability == ProviderAvailability.UNKNOWN && provider_info.priority > other_info.priority) { return -1; } if (other_availability == ProviderAvailability.UNKNOWN && other_info.priority > provider_info.priority) { return 1; } return ProviderAvailability.compare (provider_availability, other_availability); } if (provider_info.priority != other_info.priority) { return provider_info.priority > other_info.priority ? -1 : 1; } if (provider_info.selected != other_info.selected) { return provider_info.selected ? -1 : 1; } return 0; } private void get_preferred_provider_info (out unowned Ft.ProviderInfo? preferred_provider_info, out int64 preferred_provider_timeout) { unowned Ft.ProviderInfo? tmp_preferred_provider_info = null; int64 tmp_preferred_provider_timeout = 0; int64 monotonic_time = Ft.Timestamp.UNDEFINED; this.providers.@foreach ( (provider_info) => { var provider_timeout = provider_info.get_availability_timeout (ref monotonic_time); if (tmp_preferred_provider_info == null) { tmp_preferred_provider_info = provider_info; tmp_preferred_provider_timeout = provider_timeout; } else { var comparison_result = compare (tmp_preferred_provider_info, tmp_preferred_provider_timeout, provider_info, provider_timeout); if (comparison_result > 0) { tmp_preferred_provider_info = provider_info; tmp_preferred_provider_timeout = provider_timeout; } } }); preferred_provider_info = tmp_preferred_provider_info; preferred_provider_timeout = tmp_preferred_provider_timeout; } /** * Find best provider, preferably available with highest priority. */ private void select_single () { unowned Ft.ProviderInfo? preferred_provider_info = null; int64 preferred_provider_timeout = 0; var selection_changed = false; this.get_preferred_provider_info (out preferred_provider_info, out preferred_provider_timeout); if (preferred_provider_info == null) { this.select_none (); return; } if (!preferred_provider_info.instance.available_set && preferred_provider_timeout > 0 && preferred_provider_timeout < AVAILABILITY_TIMEOUT) { this.schedule_update_selection (preferred_provider_timeout); return; } if (get_availability (preferred_provider_info, preferred_provider_timeout) == ProviderAvailability.UNAVAILABLE) { this.select_none (); return; } this.providers.@foreach ( (provider_info) => { var selected = provider_info == preferred_provider_info; if (provider_info.selected != selected) { provider_info.selected = selected; selection_changed = true; if (selected) { this.provider_selected ((T) provider_info.instance); } else { this.provider_unselected ((T) provider_info.instance); } this.check_provider_status (provider_info); } }); if (selection_changed) { this.selection_changed (); } } /** * Disable all providers. */ private void select_none () { var selection_changed = false; this.providers.@foreach ( (provider_info) => { if (provider_info.selected) { provider_info.selected = false; selection_changed = true; this.provider_unselected ((T) provider_info.instance); this.check_provider_status (provider_info); } }); if (selection_changed) { this.selection_changed (); } } /** * Try enabling all providers. */ private void select_all () { var selection_changed = false; this.providers.@foreach ( (provider_info) => { if (!provider_info.selected) { provider_info.selected = true; selection_changed = true; this.provider_selected ((T) provider_info.instance); this.check_provider_status (provider_info); } }); if (selection_changed) { this.selection_changed (); } } private void update_selection () { if (this.providers == null) { return; } if (this.updating_selection) { this.selection_invalid = true; return; } if (this.update_selection_timeout_id != 0) { GLib.Source.remove (this.update_selection_timeout_id); this.update_selection_timeout_id = 0; } if (this.update_selection_idle_id != 0) { GLib.Source.remove (this.update_selection_idle_id); this.update_selection_idle_id = 0; } this.updating_selection = true; this.selection_invalid = false; switch (this._selection_mode) { case Ft.SelectionMode.NONE: this.select_none (); break; case Ft.SelectionMode.SINGLE: this.select_single (); break; case Ft.SelectionMode.ALL: this.select_all (); break; default: assert_not_reached (); } this.updating_selection = false; if (this.selection_invalid) { this.update_selection (); } } private void schedule_update_selection (int64 timeout) { if (this.update_selection_timeout_id != 0) { return; } this.update_selection_timeout_id = GLib.Timeout.add ( Ft.Timestamp.to_milliseconds_uint (timeout + AVAILABILITY_TIMEOUT_TOLERANCE), () => { this.update_selection_timeout_id = 0; this.update_selection (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.update_selection_timeout_id, "Ft.ProviderSet.schedule_update_selection"); } private void queue_update_selection () { if (this.update_selection_idle_id != 0) { return; } this.update_selection_idle_id = GLib.Idle.add ( () => { this.update_selection_idle_id = 0; this.update_selection (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.update_selection_idle_id, "Ft.ProviderSet.queue_update_selection"); } private unowned Ft.ProviderInfo? lookup_info (Ft.Provider instance) { unowned Ft.ProviderInfo provider_info = null; if (this.providers == null) { return null; } this.providers.@foreach ( (_provider_info) => { if (_provider_info.instance == instance) { provider_info = _provider_info; } }); return provider_info; } private void on_provider_notify_available (GLib.Object object, GLib.ParamSpec pspec) { var provider = (Ft.Provider) object; var provider_info = this.lookup_info (provider); if (provider_info == null) { return; } if (provider_info.selected && provider.available) { this.check_provider_status (provider_info); } else { this.update_selection (); } } private void on_provider_notify_enabled (GLib.Object object, GLib.ParamSpec pspec) { var provider = (Ft.Provider) object; if (provider.enabled) { this.provider_enabled ((T) provider); } else { this.provider_disabled ((T) provider); } } private void destroy_info (Ft.ProviderInfo provider_info) { provider_info.instance.notify["available"].disconnect (this.on_provider_notify_available); provider_info.instance.notify["enabled"].disconnect (this.on_provider_notify_enabled); provider_info.destroy (); } public void add (T provider, Ft.Priority priority = Ft.Priority.DEFAULT) { var instance = provider as Ft.Provider; assert (instance != null); if (this.providers == null) { return; } var existing_provider_info = this.lookup_info (instance); if (existing_provider_info != null) { existing_provider_info.priority = priority; } else { var provider_info = new Ft.ProviderInfo (instance, priority); if (this.providers.add (provider_info)) { provider_info.instance.notify["available"].connect (this.on_provider_notify_available); provider_info.instance.notify["enabled"].connect (this.on_provider_notify_enabled); } } if (this.should_enable) { this.update_selection (); } else { this.queue_update_selection (); } } public void remove (T provider) { var instance = provider as Ft.Provider; assert (instance != null); var provider_info = this.lookup_info (instance); if (provider_info == null || provider_info.destroying) { return; } var was_selected = provider_info.selected; if (!this.providers.remove (provider_info)) { return; } this.destroy_info (provider_info); if (was_selected) { this.update_selection (); } } public void remove_all () { if (this.providers == null) { return; } if (this.update_selection_timeout_id != 0) { GLib.Source.remove (this.update_selection_timeout_id); this.update_selection_timeout_id = 0; } if (this.update_selection_idle_id != 0) { GLib.Source.remove (this.update_selection_idle_id); this.update_selection_idle_id = 0; } Ft.ProviderInfo[] providers = {}; this.providers.@foreach ( (provider_info) => { providers += provider_info; }); this.providers.remove_all (); foreach (var provider_info in providers) { this.destroy_info (provider_info); } } private void add_extension (Peas.PluginInfo info, GLib.Object extension) { Ft.Priority priority = Ft.Priority.DEFAULT; unowned var priority_pspec = extension.get_class ().find_property ("priority"); if (priority_pspec is GLib.ParamSpecEnum) { extension.@get ("priority", ref priority); } else { priority = Ft.Priority.from_string (info.get_external_data ("Priority")); } this.add ((T) extension, priority); } /** * Initialize Peas extensions discovery. */ public void discover () { if (this.extension_set != null) { return; } var engine = Peas.Engine.get_default (); var n = engine.get_n_items (); this.extension_set = new Peas.ExtensionSet.with_properties ( engine, typeof (T), {}, {}); this.extension_set.extension_added.connect (this.on_extension_added); this.extension_set.extension_removed.connect (this.on_extension_removed); for (var i = 0U; i < n; i++) { var info = (Peas.PluginInfo) engine.get_item (i); var extension = this.extension_set.get_extension (info); if (extension != null && extension is Ft.Provider) { this.add_extension (info, extension); } } } private void on_extension_added (Peas.PluginInfo info, GLib.Object extension) { if (extension is Ft.Provider) { this.add_extension (info, extension); } } private void on_extension_removed (Peas.PluginInfo info, GLib.Object extension) { if (extension is Ft.Provider) { this.remove ((T) extension); } } public void enable () { this.should_enable = true; this.update_selection (); if (this.providers == null) { return; } this.providers.@foreach ( (provider_info) => { this.check_provider_status (provider_info); }); } public void disable () { this.should_enable = false; if (this.providers == null) { return; } this.providers.@foreach ( (provider_info) => { this.check_provider_status (provider_info); }); } public void @foreach (GLib.Func func) { if (this.providers == null) { return; } this.providers.@foreach ( (provider_info) => { func ((T) provider_info.instance); }); } public void foreach_selected (GLib.Func func) { if (this.providers == null) { return; } this.providers.@foreach ( (provider_info) => { if (provider_info.selected) { func ((T) provider_info.instance); } }); } public void foreach_enabled (GLib.Func func) { if (this.providers == null) { return; } this.providers.@foreach ( (provider_info) => { if (provider_info.instance.enabled) { func ((T) provider_info.instance); } }); } internal signal void provider_selected (T provider); internal signal void provider_unselected (T provider); public signal void provider_enabled (T provider); public signal void provider_disabled (T provider); public signal void selection_changed (); public override void dispose () { if (this.update_selection_timeout_id != 0) { GLib.Source.remove (this.update_selection_timeout_id); this.update_selection_timeout_id = 0; } if (this.update_selection_idle_id != 0) { GLib.Source.remove (this.update_selection_idle_id); this.update_selection_idle_id = 0; } if (this.extension_set != null) { this.extension_set.extension_added.disconnect (this.on_extension_added); this.extension_set.extension_removed.disconnect (this.on_extension_removed); this.extension_set = null; } this.remove_all (); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/provider.vala000066400000000000000000000061511520625676500231000ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * Provider class helps integrating with an external service. Unlike capabilities which may be enabled/disabled * according to settings, providers are enabled according to use. Another difference: for a capability * enable/disable operations should be trivial and always be successful, for providers it needs error handling. * * Subclass should contain implementation. To use it look at `ProviderSet`. */ public abstract class Provider : GLib.Object { [CCode (notify = false)] public bool available { get { return this._available; } set { if (this._available != value || !this._available_set) { var available_set_changed = !this._available_set; this._available = value; this._available_set = true; this.notify_property ("available"); if (available_set_changed) { this.notify_property ("available-set"); } } } } public bool available_set { get { return this._available_set; } } [CCode (notify = false)] public bool enabled { get { return this._enabled; } internal set { if (this._enabled != value) { this._enabled = value; this.notify_property ("enabled"); } } } private bool _available = false; private bool _available_set = false; private bool _enabled = false; /** * Set-up detection whether provider is available. Once available, the provider should set * the `available` property accordingly. * * If error happens during initialization, it's considered as uninitialized. */ public abstract async void initialize (GLib.Cancellable? cancellable) throws GLib.Error; /** * Undo the effects of `initialize` * * If error happens during uninitialization, it's still considered as uninitialized. */ public abstract async void uninitialize () throws GLib.Error; /** * Enable the provider. * * If error happens during enabling, it's considered as disabled. */ public abstract async void enable (GLib.Cancellable? cancellable) throws GLib.Error; /** * Undo the effects of `enable`. * * It may be called despite provider being unavailable. * * If error happens during disabling, it's considered as initialized; therefore * the provider should mark any registered callbacks as invalid in its internal state. */ public abstract async void disable () throws GLib.Error; } } focustimerhq-FocusTimer-8581be2/src/core/scheduler.vala000066400000000000000000000515561520625676500232350ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * Structure describing current cycle or energy level within a session. * * For simplicity, the same structure is shared between all schedulers. */ public struct SchedulerContext { public int64 timestamp; public Ft.State state; public bool is_session_completed; public bool needs_long_break; public double score; public SchedulerContext () { this.timestamp = Ft.Timestamp.UNDEFINED; this.state = Ft.State.STOPPED; this.is_session_completed = false; this.needs_long_break = false; this.score = 0.0; } public static Ft.SchedulerContext initial (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); return SchedulerContext () { timestamp = timestamp, }; } /** * Make context copy * * This function is unnecessary. Structs in vala are copied by default. It's kept * to bring more clarity to our code. */ public Ft.SchedulerContext copy () { return this; } /** * Convert structure to Variant. * * Used in tests. */ public GLib.Variant to_variant () { var builder = new GLib.VariantBuilder (new GLib.VariantType ("a{s*}")); builder.add ("{sv}", "timestamp", new GLib.Variant.int64 (this.timestamp)); builder.add ("{sv}", "state", new GLib.Variant.string (this.state.to_string ())); builder.add ("{sv}", "is_session_completed", new GLib.Variant.boolean (this.is_session_completed)); builder.add ("{sv}", "needs_long_break", new GLib.Variant.boolean (this.needs_long_break)); builder.add ("{sv}", "score", new GLib.Variant.double (this.score)); return builder.end (); } /** * Represent context as string. * * Used in tests. */ public string to_representation () { var state_string = this.state.to_string (); var representation = new GLib.StringBuilder ("SchedulerContext (\n"); representation.append (@" timestamp = $timestamp,\n"); representation.append (@" state = $state_string,\n"); representation.append (@" is_session_completed = $is_session_completed,\n"); representation.append (@" needs_long_break = $needs_long_break,\n"); representation.append (@" score = $score,\n"); representation.append (")"); return representation.str; } } /** * Scheduler helps in determining next time-block in a session. * * It works in a step-wise fashion. It tries to make a best guess according to `SchedulerContext`. * * In future we would like to align time-blocks to calendar events and working time. */ public abstract class Scheduler : GLib.Object { /** * A progress threshold that qualifies time-block to be marked as completed. * * When set to 0.5 it would count two cycles per completed Ft. Reasonable values are > 0.667. */ protected const double COMPLETION_THRESHOLD = 0.8; /** * Max number of time-blocks scheduled in case scheduler enters into an infinite loop. */ private const uint MAX_ITERATIONS = 100; [CCode (notify = false)] public Ft.SessionTemplate session_template { get { return this._session_template; } set { if (this._session_template.equals (value)) { return; } this._session_template = value; this.notify_property ("session-template"); } } private Ft.SessionTemplate _session_template; public int64 calculate_time_block_completion_time (Ft.TimeBlock time_block) { if (Ft.Timestamp.is_undefined (time_block.start_time)) { GLib.debug ("calculate_time_block_completion_time: `start_time` is not set"); return Ft.Timestamp.UNDEFINED; } var intended_duration = time_block.get_intended_duration (); if (intended_duration == 0) { intended_duration = this._session_template.get_duration (time_block.state); } var remaining_elapsed = (int64) Math.floor (intended_duration * COMPLETION_THRESHOLD); var reference_time = time_block.start_time; time_block.foreach_gap ( (gap) => { if (Ft.Timestamp.is_undefined (gap.end_time)) { return; } var tmp = remaining_elapsed - (gap.start_time - reference_time); if (tmp > 0) { remaining_elapsed = tmp; reference_time = gap.end_time; } } ); return reference_time + remaining_elapsed; } public abstract double calculate_time_block_score (Ft.TimeBlock time_block, int64 timestamp); public abstract double calculate_time_block_weight (Ft.TimeBlock time_block); /** * Update given state according to given time-block. * * The context will hold info about current state of the session. */ public abstract void resolve_context (Ft.TimeBlock time_block, bool is_resuming, int64 timestamp, ref Ft.SchedulerContext context); /** * Resolve next time-block state according to scheduler state. */ public abstract Ft.State resolve_state (Ft.SchedulerContext context); /** * Resolve next time-block according to scheduler state. */ public abstract Ft.TimeBlock? resolve_time_block (Ft.SchedulerContext context); /** * Check whether time-block has been completed at given time. */ public abstract bool is_time_block_completed (Ft.TimeBlock time_block, int64 timestamp); /** * Build a scheduler context from completed/in-progress time-blocks. */ internal void build_scheduler_context (Ft.Session session, bool is_resuming, int64 timestamp, out Ft.SchedulerContext context, out unowned GLib.List first_scheduled_link) { unowned GLib.List link = session.time_blocks.first (); context = Ft.SchedulerContext.initial ( link != null && link.data.get_status () != Ft.TimeBlockStatus.SCHEDULED ? link.data.start_time : timestamp); first_scheduled_link = null; while (link != null) { var time_block = link.data; var time_block_status = time_block.get_status (); if (time_block_status == Ft.TimeBlockStatus.SCHEDULED) { first_scheduled_link = link; break; } if (!context.is_session_completed) { var time_block_timestamp = time_block_status == Ft.TimeBlockStatus.IN_PROGRESS ? timestamp : time_block.end_time; this.resolve_context ( time_block, is_resuming, time_block_timestamp, ref context); } link = link.next; } context.timestamp = int64.max (context.timestamp, timestamp); } /** * Adjust time-block start-time and duration. */ public void reschedule_time_block (Ft.TimeBlock time_block, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (time_block.state == Ft.State.STOPPED) { return; } if (time_block.get_status () == Ft.TimeBlockStatus.COMPLETED || time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED) { return; } Ft.ensure_timestamp (ref timestamp); // TODO: adjust session template according to available time if (time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED) { var intended_duration = this._session_template.get_duration (time_block.state); time_block.set_intended_duration (intended_duration); time_block.set_time_range (timestamp, timestamp + intended_duration); } time_block.set_completion_time (this.calculate_time_block_completion_time (time_block)); time_block.set_weight (this.calculate_time_block_weight (time_block)); } public bool reschedule_session (Ft.Session session, Ft.TimeBlock? next_time_block, bool is_resuming, int64 timestamp) { Ft.ensure_timestamp (ref timestamp); Ft.SchedulerContext context; unowned GLib.List link; var initial_version = session.version; session.freeze_changed (); this.ensure_session_meta (session); this.build_scheduler_context (session, is_resuming, timestamp, out context, out link); // Prepare next time-block if (next_time_block != null) { assert (session.contains (next_time_block)); assert (next_time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Remove time-blocks leading to `next_time_block` while (link != null && link.data != next_time_block) { unowned var tmp = link.next; session.remove_link (link); link = tmp; } if (next_time_block.duration > 0) { next_time_block.move_to (timestamp); } else { this.reschedule_time_block (next_time_block, timestamp); } this.resolve_context (next_time_block, false, timestamp, ref context); link = link.next; } // Create or update time-blocks. var i = 1; while (true) { if (i++ >= Ft.Scheduler.MAX_ITERATIONS) { GLib.error ("`Session.reschedule()` reached iterations limit."); // break; } var time_block = this.resolve_time_block (context); if (time_block == null) { break; } if (link != null && link.data.state == time_block.state) { // Update existing time-block. assert (link.data.get_status () == Ft.TimeBlockStatus.SCHEDULED); link.data.set_time_range (time_block.start_time, time_block.end_time); link.data.set_meta (time_block.get_meta ()); this.resolve_context (link.data, false, timestamp, ref context); link = link.next; } else { // Add new time-block. if (link != null) { session.time_blocks.insert_before (link, time_block); } else { session.time_blocks.append (time_block); } session.emit_added (time_block); this.resolve_context (time_block, false, timestamp, ref context); } } // Remove time-blocks after a long-break. session.remove_links_after (link); session.remove_link (link); session.thaw_changed (); return session.version != initial_version; } public void ensure_time_block_meta (Ft.TimeBlock time_block) { if (time_block.get_intended_duration () == 0) { time_block.set_intended_duration ( this._session_template.get_duration (time_block.state)); } time_block.set_completion_time ( this.calculate_time_block_completion_time (time_block)); time_block.set_weight (this.calculate_time_block_weight (time_block)); } /** * Update meta for all time-blocks, not only scheduled. Intended for restoring a session. */ public void ensure_session_meta (Ft.Session session) { session.@foreach ( (time_block) => { if (time_block.state != Ft.State.STOPPED) { this.ensure_time_block_meta (time_block); } }); } } public class SimpleScheduler : Scheduler { /* Score limit helps session progress bar looking sensible, also serves as a penalty for * extending pomodoros too much */ private double MAX_SCORE = 2.0; /* Minimum intended-duration to consider scores above 1.0 */ private int64 SCORE_INTENDED_DURATION_THRESHOLD = 20 * Ft.Interval.MINUTE; public SimpleScheduler.with_template (Ft.SessionTemplate session_template) { GLib.Object ( session_template: session_template ); } private double calculate_time_block_score_internal (Ft.TimeBlock time_block, bool include_uncompleted_gaps, int64 timestamp) requires (Ft.Timestamp.is_defined (time_block.start_time)) requires (time_block.end_time >= time_block.start_time) { var intended_duration = time_block.get_intended_duration (); var score = 0.0; if (intended_duration == 0) { intended_duration = this.session_template.get_duration (time_block.state); } if (time_block.state != Ft.State.POMODORO || time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED || intended_duration <= 0) { return score; } var elapsed = time_block.calculate_elapsed (timestamp); var last_gap = time_block.get_last_gap (); if (include_uncompleted_gaps && last_gap != null && last_gap.start_time < timestamp && Ft.Timestamp.is_undefined (last_gap.end_time) && time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS) { elapsed += int64.min (timestamp, time_block.end_time) - last_gap.start_time; } if (elapsed >= SCORE_INTENDED_DURATION_THRESHOLD) { intended_duration = int64.max (intended_duration, SCORE_INTENDED_DURATION_THRESHOLD); } if (intended_duration >= SCORE_INTENDED_DURATION_THRESHOLD) { var base_score = elapsed / intended_duration; var partial_score = ( (double) (elapsed - base_score * intended_duration) / (COMPLETION_THRESHOLD * (double) intended_duration)); score = Math.floor ( (double) base_score + partial_score ).clamp (0.0, MAX_SCORE); } else { score = Math.floor ( (double) elapsed / (COMPLETION_THRESHOLD * (double) intended_duration) ).clamp (0.0, 1.0); } assert (score.is_finite ()); return score; } public override double calculate_time_block_score (Ft.TimeBlock time_block, int64 timestamp) { return this.calculate_time_block_score_internal (time_block, false, timestamp); } public override double calculate_time_block_weight (Ft.TimeBlock time_block) { return this.calculate_time_block_score_internal (time_block, true, time_block.end_time); } public override void resolve_context (Ft.TimeBlock time_block, bool is_resuming, int64 timestamp, ref Ft.SchedulerContext context) { if (time_block.state == Ft.State.STOPPED) { return; } var status = time_block.get_status (); var is_long_break = time_block.state == Ft.State.LONG_BREAK || time_block.state == Ft.State.BREAK; if (status == Ft.TimeBlockStatus.IN_PROGRESS) { var last_gap = time_block.get_last_gap (); var is_paused = last_gap != null && Ft.Timestamp.is_undefined (last_gap.end_time); if (!is_resuming && is_paused) { status = Ft.TimeBlockStatus.COMPLETED; } else { status = this.is_time_block_completed (time_block, timestamp) ? Ft.TimeBlockStatus.COMPLETED : Ft.TimeBlockStatus.UNCOMPLETED; } context.timestamp = timestamp; } else { context.timestamp = time_block.end_time; } if (status == Ft.TimeBlockStatus.SCHEDULED || status == Ft.TimeBlockStatus.COMPLETED) { context.score += this.calculate_time_block_weight (time_block); if (is_long_break) { context.is_session_completed = true; } } context.state = time_block.state; context.needs_long_break = !context.is_session_completed && context.score >= this.session_template.cycles; } public override Ft.State resolve_state (Ft.SchedulerContext context) { if (context.state != Ft.State.POMODORO) { return Ft.State.POMODORO; } if (this.session_template.has_uniform_breaks ()) { return Ft.State.BREAK; } return context.needs_long_break ? Ft.State.LONG_BREAK : Ft.State.SHORT_BREAK; } /** * Create next time-block. * * Alternate between a pomodoro and a break, regardless whether previous time-block has been completed. */ public override Ft.TimeBlock? resolve_time_block (Ft.SchedulerContext context) { if (context.is_session_completed) { return null; // Suggest starting a new session. } var state = this.resolve_state (context); var time_block = new Ft.TimeBlock (state); this.reschedule_time_block (time_block, context.timestamp); return time_block; } /** * Determine whether time-block should be marked as completed. */ public override bool is_time_block_completed (Ft.TimeBlock time_block, int64 timestamp) { var completion_time = time_block.get_completion_time (); if (Ft.Timestamp.is_undefined (completion_time)) { completion_time = this.calculate_time_block_completion_time (time_block); } return timestamp >= completion_time; } } } focustimerhq-FocusTimer-8581be2/src/core/screen-overlay-manager.vala000066400000000000000000000072311520625676500256140ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface ScreenOverlayProvider : Ft.Provider { public abstract void open (); public abstract void close (); public signal void opened (); public signal void closed (); } /** * A helper primitive for representing the screen overlay. * The actual logic for showing/hiding the overlay is in `Ft.NotificationManager`. */ [SingleInstance] public class ScreenOverlayManager : GLib.Object { public Ft.ScreenOverlayProvider? provider { get { return this._provider; } } private Ft.ProviderSet? providers = null; private unowned Ft.ScreenOverlayProvider? _provider = null; private Ft.NotificationManager? notification_manager = null; construct { this.notification_manager = new Ft.NotificationManager (); this.notification_manager.request_screen_overlay_open.connect (this.on_request_screen_overlay_open); this.notification_manager.request_screen_overlay_close.connect (this.on_request_screen_overlay_close); this.providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); this.providers.provider_selected.connect (this.on_provider_selected); this.providers.provider_unselected.connect (this.on_provider_unselected); this.providers.discover (); this.providers.enable (); } // TODO: providers should be registered staticly public void add_provider (Ft.ScreenOverlayProvider provider, Ft.Priority priority = Ft.Priority.DEFAULT) { this.providers.add (provider, priority); } public void open () { this._provider?.open (); } public void close () { this._provider?.close (); } private void on_request_screen_overlay_open () { this.open (); } private void on_request_screen_overlay_close () { this.close (); } private void on_screen_overlay_opened (Ft.ScreenOverlayProvider provider) { this.notification_manager.emit_screen_overlay_opened (); } private void on_screen_overlay_closed (Ft.ScreenOverlayProvider provider) { this.notification_manager.emit_screen_overlay_closed (); } private void on_provider_selected (Ft.ScreenOverlayProvider provider) { provider.opened.connect (this.on_screen_overlay_opened); provider.closed.connect (this.on_screen_overlay_closed); this._provider = provider; this.notify_property ("provider"); } private void on_provider_unselected (Ft.ScreenOverlayProvider provider) { provider.opened.disconnect (this.on_screen_overlay_opened); provider.closed.disconnect (this.on_screen_overlay_closed); } public override void dispose () { if (this.notification_manager != null) { this.notification_manager.request_screen_overlay_open.disconnect (this.on_request_screen_overlay_open); this.notification_manager.request_screen_overlay_close.disconnect (this.on_request_screen_overlay_close); this.notification_manager = null; } this._provider = null; this.providers = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/screen-saver.vala000066400000000000000000000035341520625676500236450ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface ScreenSaverProvider : Ft.Provider { public abstract bool active { get; } } [SingleInstance] public class ScreenSaver : Ft.ProvidedObject { public bool active { get { return this._active; } } private bool _active = false; private void update_active () { var active = this.provider != null && this.provider.enabled ? this.provider.active : false; if (this._active != active) { this._active = active; this.notify_property ("active"); } } private void on_notify_active (GLib.Object object, GLib.ParamSpec pspec) { this.update_active (); } // TODO: providers should be registered staticly public void add_provider (Ft.ScreenSaverProvider provider, Ft.Priority priority = Ft.Priority.DEFAULT) { this.providers.add (provider, priority); } protected override void initialize () { } protected override void setup_providers () { } protected override void provider_enabled (Ft.ScreenSaverProvider provider) { provider.notify["active"].connect (this.on_notify_active); this.update_active (); } protected override void provider_disabled (Ft.ScreenSaverProvider provider) { provider.notify["active"].disconnect (this.on_notify_active); this.update_active (); } } } focustimerhq-FocusTimer-8581be2/src/core/session-entry.vala000066400000000000000000000012361520625676500240670ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public class SessionEntry : Gom.Resource { public int64 id { get; set; } public int64 start_time { get; set; } public int64 end_time { get; set; } public int64 expiry_time { get; set; } internal ulong version = 0; static construct { set_table ("sessions"); set_primary_key ("id"); set_notnull ("start-time"); set_notnull ("end-time"); set_notnull ("expiry-time"); set_unique ("start-time"); } } } focustimerhq-FocusTimer-8581be2/src/core/session-manager-action-group.vala000066400000000000000000000153531520625676500267520ustar00rootroot00000000000000/* * Copyright (c) 2016-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public class SessionManagerActionGroup : GLib.SimpleActionGroup { public Ft.SessionManager session_manager { get { return this._session_manager; } construct { this._session_manager = value; this.notify_current_time_block_id = this._session_manager.notify["current-time-block"].connect ( this.on_notify_current_time_block); this.session_expired_id = this._session_manager.session_expired.connect (this.on_session_expired); } } private Ft.SessionManager _session_manager; private GLib.SimpleAction state_action; private GLib.SimpleAction start_short_break_action; private GLib.SimpleAction start_long_break_action; private GLib.SimpleAction start_break_action; private ulong notify_current_time_block_id = 0; private ulong session_expired_id = 0; public SessionManagerActionGroup () { GLib.Object ( session_manager: Ft.SessionManager.get_default () ); } construct { var advance_action = new GLib.SimpleAction ("advance", null); advance_action.activate.connect (this.activate_advance); this.add_action (advance_action); var reset_action = new GLib.SimpleAction ("reset", null); reset_action.activate.connect (this.activate_reset); this.add_action (reset_action); var state_action = new GLib.SimpleAction.stateful ("state", GLib.VariantType.STRING, new GLib.Variant.string (this.get_current_state ())); state_action.activate.connect (this.activate_state); this.add_action (state_action); var start_pomodoro_action = new GLib.SimpleAction ("start-pomodoro", null); start_pomodoro_action.activate.connect (this.activate_start_pomodoro); this.add_action (start_pomodoro_action); var start_short_break_action = new GLib.SimpleAction ("start-short-break", null); start_short_break_action.activate.connect (this.activate_start_short_break); this.add_action (start_short_break_action); var start_long_break_action = new GLib.SimpleAction ("start-long-break", null); start_long_break_action.activate.connect (this.activate_start_long_break); this.add_action (start_long_break_action); var start_break_action = new GLib.SimpleAction ("start-break", null); start_break_action.activate.connect (this.activate_start_break); this.add_action (start_break_action); this.state_action = state_action; this.start_short_break_action = start_short_break_action; this.start_long_break_action = start_long_break_action; this.start_break_action = start_break_action; } private string get_current_state () { var current_time_block = this.session_manager.current_time_block; var current_state = current_time_block != null ? current_time_block.state : Ft.State.STOPPED; return current_state.to_string (); } private void activate_advance (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.advance"); this.session_manager.advance (); } private void activate_reset (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.reset"); this.session_manager.reset (); } private void activate_state (GLib.SimpleAction action, GLib.Variant? parameter) { if (parameter == null) { return; } Ft.Context.set_event_source (@"session-manager.state:$(parameter.get_string())"); this.session_manager.advance_to_state (Ft.State.from_string (parameter.get_string ())); } private void activate_start_pomodoro (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.start-pomodoro"); this.session_manager.advance_to_state (Ft.State.POMODORO); } private void activate_start_short_break (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.start-short-break"); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); } private void activate_start_long_break (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.start-long-break"); this.session_manager.advance_to_state (Ft.State.LONG_BREAK); } private void activate_start_break (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("session-manager.start-break"); this.session_manager.advance_to_state (Ft.State.BREAK); } private void on_notify_current_time_block () { this.state_action.set_state (new Variant.string (this.get_current_state ())); } private void on_session_expired (Ft.Session session, int64 timestamp) { Ft.Context.set_event_source ("session-manager.session-expired", timestamp); } public override void dispose () { if (this.notify_current_time_block_id != 0) { this._session_manager.disconnect (this.notify_current_time_block_id); this.notify_current_time_block_id = 0; } if (this.session_expired_id != 0) { this._session_manager.disconnect (this.session_expired_id); this.session_expired_id = 0; } this.state_action = null; this.start_short_break_action = null; this.start_long_break_action = null; this.start_break_action = null; this._session_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/session-manager.vala000066400000000000000000003067361520625676500243550ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { private enum AdvancementMode { CONTINUOUS, CONFIRM, WAIT_FOR_ACTIVITY } /** * `SessionManager` sets up the timer, advances time-blocks and sessions. */ public class SessionManager : GLib.Object { /** * Idle time after which session should no longer be continued, and new session should be created. */ public const int64 SESSION_EXPIRY_TIMEOUT = Ft.Interval.HOUR; /** * Time limit when waiting for activity or confirmation. */ private const int64 OVERDUE_TIMEOUT = Ft.Interval.HOUR; private static Ft.SessionManager? instance = null; public Ft.Timer timer { get { return this._timer; } construct { this._timer = value; this._timer.reset (); this.timer_resolve_state_id = this._timer.resolve_state.connect (this.on_timer_resolve_state); this.timer_state_changed_id = this._timer.state_changed.connect (this.on_timer_state_changed); this.timer_finished_id = this._timer.finished.connect (this.on_timer_finished); this.timer_suspending_id = this._timer.suspended.connect (this.on_timer_suspending); this.timer_suspended_id = this._timer.suspended.connect (this.on_timer_suspended); } } [CCode (notify = false)] public Ft.Scheduler scheduler { get { return this._scheduler; } set { if (this._scheduler == value) { return; } if (this._scheduler != null) { this._scheduler.notify["session-template"].disconnect ( this.on_scheduler_notify_session_template); } this._scheduler = value; if (this._scheduler != null) { this._scheduler.notify["session-template"].connect ( this.on_scheduler_notify_session_template); } this.reschedule (); this.update_has_uniform_breaks (); this.notify_property ("scheduler"); } } /** * A current session. * * Setter replaces current session with another one without adjusting either of them. * To correct the timing use `advance_*` methods. After selecting session manually, the current time-block * is null and the timer is stopped. */ [CCode (notify = false)] public unowned Ft.Session current_session { get { return this._current_session; } set { this.set_current_time_block_full (value, null); } } /** * Current time-block. * * Only `SessionManager` is aware which time-block is current. Current time-block is reflected in `Timer.state`, * but it's not 1:1 equivalent. Time-block has clearly defined gaps, while timer only keeps track of offset. * * `null` means that current time-block has not yet started and that timer is stopped. * * Setter replaces current time-block with another one without adjusting either of them. * To correct the timing use `advance_*` methods. Setter is meant only for unit testing. * Time-block must be assigned to a session beforehand. Setting a time-block with a different session will * also switch to a new session. All blocks within new session preceding given `time-block` will be removed. */ [CCode (notify = false)] public unowned Ft.TimeBlock current_time_block { get { return this._current_time_block; } set { var session = value != null ? value.session : this._current_session; this.set_current_time_block_full (session, value); } } /** * Convenience property to track current state. * * The `this._current_state` is used for the property notification only. */ [CCode (notify = false)] public Ft.State current_state { get { return this._current_time_block != null ? this._current_time_block.state : Ft.State.STOPPED; } set { this.advance_to_state (value); } } public unowned Ft.Gap current_gap { get { return this._current_gap; } } /** * Convenience property to track whether there are several cycles per session. * * It equivalent to whether a session has short breaks. */ [CCode (notify = false)] public bool has_uniform_breaks { get { return this._has_uniform_breaks; } } private Ft.Timer _timer; private Ft.Scheduler _scheduler; private Ft.Session? _current_session = null; private Ft.TimeBlock? _current_time_block = null; private Ft.Gap? _current_gap = null; private Ft.State _current_state = Ft.State.STOPPED; private bool _has_uniform_breaks = false; private Ft.Session? next_session = null; private Ft.TimeBlock? next_time_block = null; private Ft.IdleMonitor? idle_monitor = null; private Ft.TimeZoneMonitor? timezone_monitor = null; private Ft.TimezoneHistory? timezone_history = null; private Ft.ScreenSaver? screensaver = null; private Ft.LockScreen? lockscreen = null; private bool auto_paused = false; private bool current_time_block_entered = false; private ulong current_time_block_changed_id = 0; private bool current_session_entered = false; private bool current_session_changed_frozen = false; private ulong current_session_notify_expiry_time_id = 0; private Ft.Session? previous_session = null; private Ft.TimeBlock? previous_time_block = null; private GLib.Settings? settings = null; private int resolving_timer_state = 0; private ulong timer_resolve_state_id = 0; private ulong timer_state_changed_id = 0; private ulong timer_finished_id = 0; private ulong timer_suspending_id = 0; private ulong timer_suspended_id = 0; private uint expiry_timeout_id = 0; private uint reschedule_idle_id = 0; private uint active_watch_id = 0; private uint session_changed_idle_id = 0; public SessionManager () { GLib.Object ( timer: Ft.Timer.get_default () ); } public SessionManager.with_timer (Ft.Timer timer) { GLib.Object ( timer: timer ); } construct { this.settings = Ft.get_settings (); this.scheduler = new Ft.SimpleScheduler (); this.timezone_monitor = new Ft.TimeZoneMonitor (); this.timezone_history = new Ft.TimezoneHistory (); this.settings.changed.connect (this.on_settings_changed); this.timezone_monitor.changed.connect (this.on_timezone_changed); this.update_session_template (); } /** * Sets a default `SessionManager`. * * The old default manager is unreffed and the new manager referenced. * * A value of null for this will cause the current default manager to be released and a new default manager * to be created on demand. */ public static void set_default (Ft.SessionManager? session_manager) { Ft.SessionManager.instance = session_manager; } /** * Return a default manager. * * A new default manager will be created on demand. */ public static unowned Ft.SessionManager get_default () { if (Ft.SessionManager.instance == null) { Ft.SessionManager.set_default (new Ft.SessionManager ()); } return Ft.SessionManager.instance; } private int64 get_current_time () { return this.resolving_timer_state > 0 ? this._timer.get_last_state_changed_time () : this._timer.get_current_time (); } private Ft.Session initialize_next_session (int64 timestamp) { var next_session = new Ft.Session (); this.reschedule_full (next_session, null, false, timestamp); return next_session; } private Ft.TimeBlock? initialize_next_time_block (int64 timestamp) { Ft.TimeBlock? next_time_block = null; Ft.Session? next_session = null; if (this._current_time_block != null && this._current_time_block.state == Ft.State.LONG_BREAK && !this._scheduler.is_time_block_completed (this._current_time_block, timestamp)) { // Continue current session. // We don't have next time-block yet. It needs to be added by scheduler. next_time_block = null; next_session = this._current_session; } else { // Try getting a scheduled block. next_time_block = this.get_next_time_block (); next_session = next_time_block != null ? next_time_block.session : null; } // Discard session if it has expired. if (next_session != null && next_session.is_expired (timestamp)) { next_session = null; } // Reschedule - update time-blocks according to given `timestamp`. if (next_session != null) { next_session.freeze_changed (); // Update break type. It's not done by scheduler, // because we want to start a desired break at will. if (next_time_block != null && next_time_block.state.is_break ()) { var next_state = Ft.State.BREAK; if (!this._has_uniform_breaks) { next_state = this.is_long_break_needed (timestamp) ? Ft.State.LONG_BREAK : Ft.State.SHORT_BREAK; } next_time_block.set_state_internal (next_state); next_time_block.duration = this.scheduler.session_template.get_duration (next_state); } this.reschedule_full (next_session, next_time_block, next_time_block != null, timestamp); next_session.thaw_changed (); if (next_time_block == null) { next_time_block = next_session == this._current_session ? next_session.get_next_time_block (this._current_time_block) : next_session.get_first_time_block (); } } if (next_session == null || next_time_block == null) { next_session = this.initialize_next_session (timestamp); next_time_block = next_session.get_first_time_block (); } assert (next_time_block != null); assert (next_time_block.session == next_session); // Store `this.next_session` only to increase session ref count this.next_session = next_session; this.next_time_block = next_time_block; return next_time_block; } /** * Builds a `TimerState` according to current time-block. */ private void initialize_timer_state (ref Ft.TimerState state, int64 timestamp) { var current_time_block = this._current_time_block; var current_gap = this._current_gap; if (current_time_block == null) { state = Ft.TimerState (); return; } var paused_time = current_gap != null && Ft.Timestamp.is_undefined (current_gap.end_time) ? current_gap.start_time : Ft.Timestamp.UNDEFINED; if (Ft.Timestamp.is_defined (paused_time)) { timestamp = paused_time; } timestamp = timestamp.clamp (current_time_block.start_time, current_time_block.end_time); // Adjust timer state according to the current-time-block. if (!this.is_waiting_for_activity ()) { assert (Ft.Timestamp.is_defined (current_time_block.start_time)); var elapsed = current_time_block.calculate_elapsed (timestamp); state.offset = timestamp - current_time_block.start_time - elapsed; state.duration = current_time_block.duration - state.offset; state.started_time = current_time_block.start_time; state.paused_time = paused_time; state.finished_time = Ft.Timestamp.UNDEFINED; if (timestamp >= current_time_block.end_time && Ft.Timestamp.is_undefined (paused_time)) { state.finished_time = current_time_block.end_time; } } else { assert (current_gap == null); state.offset = 0; state.duration = current_time_block.duration; state.started_time = Ft.Timestamp.UNDEFINED; state.paused_time = Ft.Timestamp.UNDEFINED; state.finished_time = Ft.Timestamp.UNDEFINED; } state.user_data = current_time_block; } private void update_session_template () { this._scheduler.session_template = Ft.SessionTemplate.with_defaults (); } private void update_timer_state (int64 timestamp) { if (this.resolving_timer_state != 0) { return; } if (this._current_time_block != null) { var state = Ft.TimerState (); this.initialize_timer_state (ref state, timestamp); this._timer.set_state_full (state, timestamp); } else { this._timer.reset (0, null, timestamp); } } private void update_has_uniform_breaks () { var has_uniform_breaks = this._scheduler.session_template.has_uniform_breaks (); if (has_uniform_breaks != this._has_uniform_breaks) { this._has_uniform_breaks = has_uniform_breaks; this.notify_property ("has-uniform-breaks"); } if (this._current_time_block != null && this._current_time_block.state == Ft.State.BREAK && !has_uniform_breaks) { this._current_time_block.set_state_internal (Ft.State.SHORT_BREAK); } if (this._current_time_block != null && this._current_time_block.state == Ft.State.SHORT_BREAK && has_uniform_breaks) { this._current_time_block.set_state_internal (Ft.State.BREAK); } } private unowned Ft.TimeBlock schedule_pomodoro (int64 timestamp) requires (this._current_session != null) { Ft.SchedulerContext context; unowned Ft.TimeBlock? next_time_block = null; unowned var link = this._current_session.time_blocks.first (); // Pick first scheduled pomodoro while (link != null) { if (link.data.get_status () == Ft.TimeBlockStatus.SCHEDULED && link.data.state == Ft.State.POMODORO) { next_time_block = link.data; break; } link = link.next; } // Ensure a pomodoro is scheduled if (next_time_block == null) { this._scheduler.build_scheduler_context (this._current_session, true, timestamp, out context, out link); context.timestamp = timestamp; context.state = Ft.State.STOPPED; var resolved_time_block = this._scheduler.resolve_time_block (context); if (link != null) { this._current_session.insert_before (resolved_time_block, link.data); } else { this._current_session.append (resolved_time_block); } next_time_block = resolved_time_block; } else { this._scheduler.reschedule_time_block (next_time_block, timestamp); } return next_time_block; } /** * Reschedule session having a pre-selected upcoming time-block. */ private void reschedule_full (Ft.Session? session, Ft.TimeBlock? next_time_block, bool next_time_block_set, int64 timestamp) requires (next_time_block_set || next_time_block == null) { if (session == null || session.is_completed () || this._scheduler == null) { return; } if (next_time_block_set && next_time_block != null && next_time_block.get_status () != Ft.TimeBlockStatus.SCHEDULED) { next_time_block_set = false; next_time_block = null; } if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = this.get_current_time (); } session.freeze_changed (); if (next_time_block_set && next_time_block == null && session == this._current_session) { // Anticipate a pomodoro after stopping the timer var scheduled_time_block = this.get_next_time_block (); if (scheduled_time_block == null || scheduled_time_block.state != Ft.State.POMODORO) { next_time_block = this.schedule_pomodoro (timestamp); } } var rescheduled = this._scheduler.reschedule_session (session, next_time_block, next_time_block_set, timestamp); if (session == this._current_session && rescheduled) { this.emit_session_rescheduled (timestamp); } session.thaw_changed (); } /** * Try reschedule current session. */ private void reschedule () { if (this.reschedule_idle_id != 0) { GLib.Source.remove (this.reschedule_idle_id); this.reschedule_idle_id = 0; } if (this._current_session == null) { return; } var timestamp = this._current_time_block != null ? this._current_time_block.end_time : this.get_current_time (); this.reschedule_full (this._current_session, null, false, timestamp); } /** * Try reschedule current session, if it was queued. */ private void reschedule_if_queued () { if (this.reschedule_idle_id != 0) { this.reschedule (); } } private void queue_reschedule () { if (this._current_session == null) { return; } if (this.reschedule_idle_id != 0) { return; } this.reschedule_idle_id = GLib.Idle.add ( () => { this.reschedule_idle_id = 0; this.reschedule (); return GLib.Source.REMOVE; }, GLib.Priority.HIGH_IDLE ); GLib.Source.set_name_by_id (this.reschedule_idle_id, "Ft.SessionManager.reschedule"); } /** * Set current time-block. * * It may ignore given session if it's completed. */ private void set_current_time_block_full (Ft.Session? session, Ft.TimeBlock? time_block, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (session == this._current_session && time_block == this._current_time_block) { // XXX: Handle `gap`? return; } Ft.ensure_timestamp (ref timestamp); if (session != null && session.is_expired (timestamp)) { GLib.debug ("set_current_time_block_full: setting an expired session"); } var previous_session = this._current_session; var previous_time_block = this._current_time_block; var previous_gap = this._current_gap; var previous_state = this._current_state; var state = time_block != null ? time_block.state : Ft.State.STOPPED; var gap = time_block?.get_last_gap (); if (gap != null && Ft.Timestamp.is_defined (gap.end_time)) { gap = null; } this.freeze_current_session_changed (); // Leave previous time-block. if (previous_time_block != null) { // Prevent `TimeBlock.changed` signal from triggering rescheduling. if (this.current_time_block_changed_id != 0) { GLib.SignalHandler.block (previous_time_block, this.current_time_block_changed_id); } if (previous_gap != null) { this.mark_gap_end (previous_gap, timestamp); } if (previous_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS) { this.mark_time_block_end (previous_time_block, timestamp); } if (this.current_time_block_entered) { this.leave_time_block (previous_time_block); } if (this._current_time_block != previous_time_block || this._current_session != previous_session) { GLib.debug ("The time-block was changed during `leave-time-block` emission."); return; } } if (session != null && session.is_completed ()) { session = null; } // Leave previous session. if (previous_session != null && session != previous_session) { this.mark_session_end (previous_session, timestamp); if (this.current_session_entered) { this.leave_session (previous_session); } if (this._current_time_block != previous_time_block || this._current_session != previous_session) { GLib.debug ("The time-block was changed during `leave-session` emission."); return; } } // Reschedule. if (session != null) { if (session == this.next_session && time_block == this.next_time_block && time_block != null && time_block.start_time == timestamp) { // Already scheduled using `initialize_next_time_block` } else { this.reschedule_full (session, time_block, true, timestamp); } } // Enter session. if (session != previous_session) { this.previous_session = previous_session; this.previous_time_block = previous_time_block; this._current_session = session; this._current_time_block = null; this._current_gap = null; this.notify_property ("current-session"); if (session != null) { this.enter_session (session); } if (this._current_session != session) { GLib.debug ("The session was changed during `enter-session` emission."); return; } if (this._current_time_block != null) { GLib.debug ("The time-block was changed during `enter-session` emission."); return; } } // Enter time-block. It will start or stop the timer depending whether time-block is null. if (time_block != previous_time_block) { this.previous_time_block = previous_time_block; this._current_time_block = time_block; this._current_gap = gap; this._current_state = state; this.notify_property ("current-time-block"); if (state != previous_state) { this.notify_property ("current-state"); } if (time_block != null) { this.enter_time_block (time_block); } if (this._current_time_block != time_block) { GLib.debug ("The time-block was changed during `enter-time-block` emission."); return; } this.update_timer_state (timestamp); } this.reschedule_if_queued (); this.advanced (session, time_block, previous_session, previous_time_block); if (this._current_time_block != null) { this.enable_idle_monitor (); } else { this.disable_idle_monitor (); } this.thaw_current_session_changed (); } public unowned Ft.TimeBlock? get_next_time_block () { Ft.TimeBlock? next_time_block = null; if (this._current_session != null && this._current_time_block == null) { if (this.previous_time_block != null && this.previous_time_block.session == this._current_session) { next_time_block = this._current_session.get_next_time_block (this.previous_time_block); } else { next_time_block = this._current_session.get_first_time_block (); } } else if (this._current_session != null) { next_time_block = this._current_session.get_next_time_block (this._current_time_block); } // Jump to the first scheduled time-block. Used for restoring. while (next_time_block != null && next_time_block.get_status () != Ft.TimeBlockStatus.SCHEDULED) { next_time_block = this._current_session.get_next_time_block (next_time_block); } return next_time_block != null ? next_time_block : this.next_time_block; } /** * Return cycle associated with current_time_block. */ public unowned Ft.Cycle? get_current_cycle () { var current_time_block = this._current_time_block != null ? this._current_time_block : this.previous_time_block; if (current_time_block == null || this._current_session == null) { return null; } var cycles = this._current_session.get_cycles (); // XXX: makes list copy unowned GLib.List link = cycles.first (); while (link != null) { if (link.data.contains (current_time_block)) { return link.data; } link = link.next; } return null; } private Ft.AdvancementMode resolve_advancement_mode (Ft.State next_state, int64 timestamp) requires (this.idle_monitor != null) { // Pick appropriate advancement mode according to current state. var current_state = this._current_time_block != null ? this._current_time_block.state : Ft.State.STOPPED; var advancement_mode = Ft.AdvancementMode.CONTINUOUS; if (current_state == Ft.State.POMODORO) { advancement_mode = this.settings.get_boolean ("confirm-starting-break") ? Ft.AdvancementMode.CONFIRM : Ft.AdvancementMode.CONTINUOUS; } else if (current_state.is_break ()) { advancement_mode = this.settings.get_boolean ("confirm-starting-pomodoro") ? Ft.AdvancementMode.CONFIRM : Ft.AdvancementMode.WAIT_FOR_ACTIVITY; } switch (advancement_mode) { case Ft.AdvancementMode.WAIT_FOR_ACTIVITY: // If `IdleMonitor` is not available, use manual confirmation. if (!this.idle_monitor.enabled) { advancement_mode = Ft.AdvancementMode.CONFIRM; } break; default: break; } return advancement_mode; } /** * Push the removal of extra time-blocks onto SQLite queue. */ private void delete_time_blocks (Gom.Repository repository, int64 session_id, GLib.Array time_block_ids_to_keep) requires (session_id != 0) { var adapter = repository.adapter; assert (adapter != null); var session_id_value = GLib.Value (typeof (int64)); session_id_value.set_int64 (session_id); Gom.Filter[] filters = { new Gom.Filter.eq (typeof (Ft.TimeBlockEntry), "session-id", session_id_value), }; for (var index = 0; index < time_block_ids_to_keep.length; index++) { var time_block_id = time_block_ids_to_keep.index (index); var time_block_id_value = GLib.Value (typeof (int64)); time_block_id_value.set_int64 (time_block_id); filters += new Gom.Filter.neq (typeof (Ft.TimeBlockEntry), "id", time_block_id_value); } var filter = new Gom.Filter.and_full (filters); var command_builder = (Gom.CommandBuilder) GLib.Object.@new ( typeof (Gom.CommandBuilder), resource_type: typeof (Ft.TimeBlockEntry), adapter: adapter, filter: filter); var command = command_builder.build_delete (); command.@ref (); adapter.queue_write ( () => { try { command.execute (null); } catch (GLib.Error error) { GLib.warning ("Error while deleting time-blocks: %s", error.message); } command.@unref (); }); } private void delete_gaps (Gom.Repository repository, int64 time_block_id, GLib.Array gap_ids_to_keep) requires (time_block_id != 0) { var adapter = repository.adapter; assert (adapter != null); var time_block_id_value = GLib.Value (typeof (int64)); time_block_id_value.set_int64 (time_block_id); Gom.Filter[] filters = { new Gom.Filter.eq (typeof (Ft.GapEntry), "time-block-id", time_block_id_value), }; for (var index = 0; index < gap_ids_to_keep.length; index++) { var gap_id = gap_ids_to_keep.index (index); var gap_id_value = GLib.Value (typeof (int64)); gap_id_value.set_int64 (gap_id); filters += new Gom.Filter.neq (typeof (Ft.GapEntry), "id", gap_id_value); } var filter = new Gom.Filter.and_full (filters); var command_builder = (Gom.CommandBuilder) GLib.Object.@new ( typeof (Gom.CommandBuilder), resource_type: typeof (Ft.GapEntry), adapter: adapter, filter: filter); var command = command_builder.build_delete (); command.@ref (); adapter.queue_write ( () => { try { command.execute (null); } catch (GLib.Error error) { GLib.warning ("Error while deleting gaps: %s", error.message); } command.@unref (); }); } /** * Delete a session entry by ID, working around a floating reference bug in Gom. * * Gom's `GomResource.delete_async()` has a bug where it creates a filter with a floating * reference and unrefs it without sinking, causing a critical warning. This method * implements the delete operation directly to avoid that issue. * * XXX: fix it upstream */ private void delete_session (Gom.Repository repository, int64 session_id) requires (session_id != 0) { var adapter = repository.adapter; assert (adapter != null); var id_value = GLib.Value (typeof (int64)); id_value.set_int64 (session_id); var filter = new Gom.Filter.eq (typeof (Ft.SessionEntry), "id", id_value); var command_builder = (Gom.CommandBuilder) GLib.Object.@new ( typeof (Gom.CommandBuilder), resource_type: typeof (Ft.SessionEntry), adapter: adapter, filter: filter); var command = command_builder.build_delete (); command.@ref (); adapter.queue_write ( () => { try { command.execute (null); } catch (GLib.Error error) { GLib.warning ("Error while deleting session entry: %s", error.message); } command.@unref (); }); } private async void save_session (Gom.Repository repository, Ft.Session session) throws GLib.Error { if (!session.should_create_entry ()) { if (session.entry != null && session.entry.id != 0) { var session_entry = session.entry; session.unset_entry (); // yield session_entry.delete_async (); // XXX: should be async this.delete_session (repository, session_entry.id); } return; } // Collect session data. var session_entry_needs_update = session.should_update_entry (); var session_entry = session.create_or_update_entry (); var keep_time_block_ids = new GLib.Array (); var keep_gap_ids = new GLib.HashTable.full ( GLib.direct_hash, GLib.direct_equal, null, (arr) => { ((GLib.Array) arr).data = null; }); var time_block_mapping = new GLib.HashTable (GLib.direct_hash, GLib.direct_equal); var gap_mapping = new GLib.HashTable (GLib.direct_hash, GLib.direct_equal); // Ensure expiry time is defined. if (Ft.Timestamp.is_undefined (session_entry.expiry_time)) { session_entry.expiry_time = this.get_current_time () + SESSION_EXPIRY_TIMEOUT; session_entry_needs_update = true; } session.@foreach ( (time_block) => { if (!time_block.should_create_entry ()) { return; } var time_block_entry = time_block.entry; if (time_block_entry != null && time_block_entry.id != 0) { keep_time_block_ids.append_val (time_block_entry.id); } if (time_block.should_update_entry ()) { var gap_ids = new GLib.Array (); time_block_entry = time_block.create_or_update_entry (); time_block_mapping.insert (time_block, time_block_entry); time_block.foreach_gap ( (gap) => { if (!gap.should_create_entry ()) { return; } var gap_entry = gap.entry; if (gap_entry != null && time_block_entry.id != 0) { gap_ids.append_val (gap_entry.id); } if (gap.should_update_entry ()) { gap_entry = gap.create_or_update_entry (); gap_mapping.insert (gap, gap_entry); } }); if (time_block_entry.id != 0) { keep_gap_ids.insert (time_block_entry.id, gap_ids); } } }); // Commit updates. try { if (session_entry_needs_update) { yield session_entry.save_async (); } var time_block_entries = (Gom.ResourceGroup) GLib.Object.@new ( typeof (Gom.ResourceGroup), repository: repository, resource_type: typeof (Ft.TimeBlockEntry), is_writable: true); time_block_mapping.@foreach ( (time_block, time_block_entry) => { assert (time_block_entry.session_id != 0); // HACK: Gom is eager to erase is-from-table flag, // which leads entries to being inserted again, not updated. time_block_entry.set_data ("is-from-table", time_block_entry.id != 0); time_block_entries.append (time_block_entry); }); this.delete_time_blocks (repository, session_entry.id, keep_time_block_ids); time_block_entries.write_sync (); // yield time_block_entries.write_async (); // XXX: should use async time_block_mapping.@foreach ( (time_block, time_block_entry) => { // HACK: Tell Gom that entries are from db time_block_entry.set_data ("is-from-table", true); this.time_block_saved (time_block, time_block_entry); }); var gap_entries = (Gom.ResourceGroup) GLib.Object.@new ( typeof (Gom.ResourceGroup), repository: repository, resource_type: typeof (Ft.TimeBlockEntry), is_writable: true); gap_mapping.@foreach ( (gap, gap_entry) => { assert (gap_entry.time_block_id != 0); gap_entries.append (gap_entry); }); keep_gap_ids.@foreach ( (time_block_id, gap_ids_to_keep) => { this.delete_gaps (repository, time_block_id, gap_ids_to_keep); }); gap_entries.write_sync (); // yield gap_entries.write_async (); // XXX: should use async gap_mapping.@foreach ( (gap, gap_entry) => { // HACK: Tell Gom that entries are from db gap_entry.set_data ("is-from-table", true); this.gap_saved (gap, gap_entry); }); } catch (GLib.Error error) { throw error; } // TODO: check if instances are disposed properly } /** * Save session state to database. */ public async bool save () { var repository = Ft.Database.get_repository (); assert (repository != null); var remaining_sessions = 0; var success = true; if (this.previous_session != null) { remaining_sessions++; this.save_session.begin ( repository, this.previous_session, (obj, res) => { try { this.save_session.end (res); } catch (GLib.Error error) { GLib.warning ("Error while saving previous session: %s", error.message); success = false; } remaining_sessions--; if (remaining_sessions == 0) { this.save.callback (); } }); } if (this._current_session != null) { remaining_sessions++; this.save_session.begin ( repository, this._current_session, (obj, res) => { try { this.save_session.end (res); } catch (GLib.Error error) { GLib.warning ("Error while saving current session: %s", error.message); success = false; } remaining_sessions--; if (remaining_sessions == 0) { this.save.callback (); } }); } if (remaining_sessions > 0) { yield; } return success; } private async Ft.Session? restore_session (Gom.Repository repository, Ft.SessionEntry session_entry) throws GLib.Error { // Load time blocks for this session var session_id_value = GLib.Value (typeof (int64)); session_id_value.set_int64 (session_entry.id); var time_block_filter = new Gom.Filter.eq ( typeof (Ft.TimeBlockEntry), "session-id", session_id_value); var time_block_sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); time_block_sorting.add ( typeof (Ft.TimeBlockEntry), "start-time", Gom.SortingMode.ASCENDING); var time_block_results = yield repository.find_sorted_async ( typeof (Ft.TimeBlockEntry), time_block_filter, time_block_sorting); if (time_block_results.count == 0) { return null; } yield time_block_results.fetch_async (0, time_block_results.count); // Reconstruct the session var session = new Ft.Session (); session.expiry_time = session_entry.expiry_time; session.entry = session_entry; // Build mapping from time_block_entry.id to time_block var time_blocks_by_id = new GLib.HashTable ( GLib.int64_hash, GLib.int64_equal); Gom.Filter[] gap_filters = {}; for (var i = 0; i < time_block_results.count; i++) { var time_block_entry = (Ft.TimeBlockEntry) time_block_results.get_index (i); var time_block = new Ft.TimeBlock (Ft.State.from_string (time_block_entry.state)); time_block.set_time_range (time_block_entry.start_time, time_block_entry.end_time); time_block.set_status (Ft.TimeBlockStatus.from_string (time_block_entry.status)); time_block.set_intended_duration (time_block_entry.intended_duration); time_block.entry = time_block_entry; time_blocks_by_id.insert (time_block_entry.id, time_block); // Build filter for this time block's gaps var time_block_id_value = GLib.Value (typeof (int64)); time_block_id_value.set_int64 (time_block_entry.id); gap_filters += new Gom.Filter.eq (typeof (Ft.GapEntry), "time-block-id", time_block_id_value); session.append (time_block); } // Load all gaps for this session if (gap_filters.length > 0) { var gap_filter = gap_filters.length == 1 ? gap_filters[0] : new Gom.Filter.or_full (gap_filters); var gap_sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); gap_sorting.add (typeof (Ft.GapEntry), "start-time", Gom.SortingMode.ASCENDING); var gap_results = yield repository.find_sorted_async (typeof (Ft.GapEntry), gap_filter, gap_sorting); yield gap_results.fetch_async (0, gap_results.count); // Distribute gaps to their respective time blocks for (var j = 0; j < gap_results.count; j++) { var gap_entry = (Ft.GapEntry) gap_results.get_index (j); var time_block = time_blocks_by_id.lookup (gap_entry.time_block_id); var gap = new Ft.Gap (Ft.GapFlags.from_string (gap_entry.flags)); gap.set_time_range (gap_entry.start_time, gap_entry.end_time); gap.entry = gap_entry; time_block.add_gap (gap); } } session.@foreach ((time_block) => time_block.normalize_gaps ()); this._scheduler.ensure_session_meta (session); return session; } /** * Restore session from database. */ public async bool restore (int64 timestamp) { Ft.SessionEntry? session_entry = null; Ft.Session? restored_session = null; Ft.TimeBlock? restored_time_block = null; Ft.Gap? restored_gap = null; Ft.ensure_timestamp (ref timestamp); var repository = Ft.Database.get_repository (); var success = true; assert (repository != null); // XXX: We're trying to write a timezone entry at first opportunity after the // database is ready. Not the best place to do that. this.mark_timezone (this.timezone_monitor.timezone, timestamp); // Query the most recent session var end_time_value = GLib.Value (typeof (int64)); end_time_value.set_int64 (timestamp - SESSION_EXPIRY_TIMEOUT); var session_filter = new Gom.Filter.gte ( typeof (Ft.SessionEntry), "end-time", end_time_value); var session_sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); session_sorting.add ( typeof (Ft.SessionEntry), "start-time", Gom.SortingMode.DESCENDING); try { var session_results = yield repository.find_sorted_async ( typeof (Ft.SessionEntry), session_filter, session_sorting); if (session_results.count > 0) { yield session_results.fetch_async (0, 1); session_entry = (Ft.SessionEntry) session_results.get_index (0); } } catch (GLib.Error error) { GLib.warning ("Unable to fetch last session: %s", error.message); success = false; } if (session_entry != null) { try { restored_session = yield this.restore_session (repository, session_entry); } catch (GLib.Error error) { GLib.warning ("Unable to restore session #%s: %s", session_entry.id.to_string (), error.message); success = false; } } if (restored_session != null) { restored_session.@foreach ( (time_block) => { if (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS) { restored_time_block = time_block; } }); restored_time_block?.foreach_gap ( (gap) => { if (Ft.Timestamp.is_undefined (gap.end_time)) { restored_gap = gap; } }); if (restored_time_block != null && restored_time_block.end_time > timestamp && restored_gap == null) { GLib.warning ("Restoring a time-block that hasn't been paused. Rewinding to last known position..."); restored_gap = restored_time_block.get_last_gap (); if (restored_gap != null) { restored_gap.end_time = Ft.Timestamp.UNDEFINED; } else { restored_time_block.set_status (Ft.TimeBlockStatus.SCHEDULED); restored_time_block = null; } } if (!restored_session.is_completed () && !restored_session.is_expired (timestamp)) { this.set_current_time_block_full (restored_session, restored_time_block, timestamp); } if (this._timer.is_finished () && this._timer.user_data == restored_time_block) { this.confirm_advancement (this._current_time_block, this.initialize_next_time_block (timestamp)); } } return success; } /** * Extend current time-block, as if it was started from scratch. */ private void extend_current_time_block (bool resume, int64 timestamp) requires (this._current_time_block != null) requires (this._current_time_block == this._timer.user_data) { var duration = this._timer.calculate_elapsed (timestamp) + this._current_time_block.get_intended_duration (); // TODO: use scheduler to determine `duration` if (this._timer.is_finished ()) { duration += timestamp - this._timer.state.finished_time; } if (resume && this._timer.is_paused ()) { var new_state = this._timer.state.copy (); new_state.duration = duration; new_state.offset += timestamp - new_state.paused_time; new_state.paused_time = Ft.Timestamp.UNDEFINED; this._timer.set_state_full (new_state, timestamp); } else { this._timer.set_duration_full (duration, timestamp); } } private void swap_current_state (Ft.State new_state, int64 timestamp) requires (this._current_time_block != null) requires (this._current_state.is_break ()) { var time_block = this._current_time_block; time_block.freeze_changed (); time_block.set_state_internal (new_state); time_block.set_intended_duration ( this._scheduler.session_template.get_duration (time_block.state)); time_block.set_completion_time ( this._scheduler.calculate_time_block_completion_time (time_block)); this.extend_current_time_block (true, timestamp); time_block.thaw_changed (); } private void mark_gap_end (Ft.Gap gap, int64 timestamp) requires (timestamp >= gap.start_time) { gap.end_time = timestamp; // Invalidate interruption when stopping the timer // TODO: also invalidate interruption on session expiry if (gap.has_flag (Ft.GapFlags.INTERRUPTION) && Ft.Context.get_event_source () == "timer.reset") { gap.unset_flag (Ft.GapFlags.INTERRUPTION); } } /** * Adjust time-blocks `end-time` and its status. */ private void mark_time_block_end (Ft.TimeBlock time_block, int64 timestamp) requires (time_block.get_status () != Ft.TimeBlockStatus.SCHEDULED) requires (timestamp >= time_block.start_time) { time_block.freeze_changed (); if (this._timer.is_finished () && this._timer.user_data == time_block) { // Count overdue time when confirming advancement time_block.end_time = int64.min ( this._timer.state.finished_time + OVERDUE_TIMEOUT, timestamp); } else { time_block.end_time = timestamp; } time_block.foreach_gap ( (gap) => { gap.end_time = Ft.Timestamp.is_undefined (gap.end_time) ? time_block.end_time : int64.min (gap.end_time, timestamp); }); time_block.set_status ( this._scheduler.is_time_block_completed (time_block, time_block.end_time) ? Ft.TimeBlockStatus.COMPLETED : Ft.TimeBlockStatus.UNCOMPLETED); this.update_time_block_meta (time_block); time_block.thaw_changed (); } /** * Discard time-blocks that were not marked as ended. * Ensure there is no in-progress time block. */ private void mark_session_end (Ft.Session session, int64 timestamp) { session.freeze_changed (); session.remove_scheduled (); var last_time_block = session.get_last_time_block (); if (last_time_block != null) { // Time-block should be marked as ended earlier, and leave-time-block should be already emitted. assert (last_time_block.get_status () != Ft.TimeBlockStatus.IN_PROGRESS); } session.expiry_time = timestamp; session.thaw_changed (); } private void mark_timezone (GLib.TimeZone timezone, int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); this.timezone_history.insert (timestamp, timezone); } private bool is_long_break_needed (int64 timestamp) { Ft.SchedulerContext context; if (this._current_session == null) { return false; } this._scheduler.build_scheduler_context (this._current_session, true, timestamp, out context, null); return context.needs_long_break; } private bool is_current_session_completed (int64 timestamp) { if (this._current_session == null) { return false; } if (this._current_time_block == null) { return this._current_session.is_completed (); } if (this._current_session.is_completed ()) { return true; } if (this._current_time_block != null && ( this._current_time_block.state == LONG_BREAK || this._current_time_block.state == BREAK)) { return this._scheduler.is_time_block_completed (this._current_time_block, timestamp); } return false; } private inline bool is_waiting_for_activity () { return this.active_watch_id != 0; } /** * Start given time-block. * * It ends previous time-block and switches to a new one, even if it's of same state. * There are many similarities with `set_current_time_block()`, except for handling * timer stop (passing `null` time-block). `advance_to_time_block` ensures we advance to * another session instead of `null`. * * When we advance to a break - its type may be change during rescheduling. */ private void advance_to_time_block (Ft.TimeBlock? time_block, int64 timestamp = Ft.Timestamp.UNDEFINED) { assert (time_block == null || time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED); Ft.ensure_timestamp (ref timestamp); var session = time_block != null ? time_block.session : this._current_session; if (time_block != null && session != null && session.is_expired (timestamp)) { GLib.warning ("Advancing to a time-block of expired session."); } if (time_block == null && ( this._current_session == null || this.is_current_session_completed (timestamp))) { session = this.initialize_next_session (timestamp); } this.set_current_time_block_full (session, time_block, timestamp); } /** * Switch to a given state. * * Unlike `advance_to_time_block()`, it tries to extend current time-block and can handle Ft.State.BREAK. * This is the preferred method of advancing the timer states as it tries to avoid emitting enter- leave- * signals unnecessarily. Switching between break types will cause creation a of a new time-block. * * This method allows you to force a short or long break. */ public void advance_to_state (Ft.State state, int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); if (state == Ft.State.STOPPED) { this.advance_to_time_block (null, timestamp); return; } // Determine the type of break if Ft.State.BREAK is given. // It's not efficient, as we build scheduler context again later. if (state == Ft.State.BREAK && !this._has_uniform_breaks) { state = this.is_long_break_needed (timestamp) ? Ft.State.LONG_BREAK : Ft.State.SHORT_BREAK; } // Extend current time-block if possible. if (this._current_time_block != null && this._current_time_block.state == state) { this.extend_current_time_block (true, timestamp); return; } // Swap break state. if (this._current_time_block != null && this._current_time_block.state.is_break () && state.is_break ()) { this.swap_current_state (state, timestamp); return; } this.freeze_current_session_changed (); // Preserve current session if it hasn't been completed. var next_time_block = this.get_next_time_block (); var next_session = next_time_block != null ? next_time_block.session : null; if (next_session == null && this._current_session != null && !this._current_session.is_completed ()) { next_session = this._current_session; next_time_block = null; } // Check whether session has expired and select upcoming time-block. if (next_session == null || next_session.is_expired (timestamp)) { next_session = this.initialize_next_session (timestamp); next_time_block = next_session.get_first_time_block (); } // Create a time-block for given state. if (next_time_block != null && next_time_block.state != state) { var time_block = new Ft.TimeBlock.with_start_time (timestamp, state); next_session.insert_before (time_block, next_time_block); next_time_block = time_block; } else if (next_time_block == null) { var time_block = new Ft.TimeBlock.with_start_time (timestamp, state); next_session.append (time_block); next_time_block = time_block; } this.advance_to_time_block (next_time_block, timestamp); this.thaw_current_session_changed (); } /** * Jump to next scheduled time-block. */ public void advance (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); this.freeze_current_session_changed (); var next_time_block = this.initialize_next_time_block (timestamp); this.advance_to_time_block (next_time_block, timestamp); this.thaw_current_session_changed (); } /** * Update session `expiry-time`. * * The given timeout is relative to last action in the session. */ private void bump_expiry_time (int64 timeout) { if (this.expiry_timeout_id != 0) { GLib.Source.remove (this.expiry_timeout_id); this.expiry_timeout_id = 0; } if (this._current_session != null && !this._current_session.is_scheduled ()) { var expiry_time = this._timer.is_running () ? Ft.Timestamp.UNDEFINED : this._timer.get_last_state_changed_time () + timeout; GLib.SignalHandler.block ( this._current_session, this.current_session_notify_expiry_time_id); this._current_session.expiry_time = expiry_time; GLib.SignalHandler.unblock ( this._current_session, this.current_session_notify_expiry_time_id); // Call notify signal handler even if there is no change. this.on_current_session_notify_expiry_time (); } } private void expire_current_session (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this._current_session == null || this._current_session.is_scheduled ()) { return; } Ft.ensure_timestamp (ref timestamp); var current_session = this._current_session; this.session_expired (this._current_session, timestamp); if (this._current_session == current_session) { this.reset (timestamp); } else { GLib.debug ("The session was changed during `session-expired` emission."); } } private void freeze_current_session_changed () { if (this._current_session != null && !this.current_session_changed_frozen) { this.current_session_changed_frozen = true; this._current_session.freeze_changed (); this.session_changed_idle_id = GLib.Idle.add ( () => { GLib.debug ("Calling `thaw_current_session_changed` late..."); this.session_changed_idle_id = 0U; this.thaw_current_session_changed (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.session_changed_idle_id, "Ft.SessionManager.thaw_current_session_changed"); } } private void thaw_current_session_changed () { if (this.session_changed_idle_id != 0U) { GLib.Source.remove (this.session_changed_idle_id); this.session_changed_idle_id = 0U; } if (this.current_session_changed_frozen) { assert (this._current_session != null); this.current_session_changed_frozen = false; this._current_session.thaw_changed (); } } private void emit_session_rescheduled (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this._current_session == null) { return; } if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = this.get_current_time (); } this.session_rescheduled (this._current_session, timestamp); } private bool should_auto_pause () { if (this._current_time_block == null || this._current_time_block.state != Ft.State.POMODORO || !this._timer.is_running () && !this.auto_paused || this.resolving_timer_state > 0) { return false; } if (this.lockscreen != null && this.lockscreen.enabled && this.lockscreen.active) { return true; } if (this.screensaver != null && this.screensaver.enabled && this.screensaver.active) { return true; } return false; } private bool should_auto_resume () { if (!this.auto_paused) { return false; } if (this.lockscreen != null && this.lockscreen.enabled && this.lockscreen.active) { return false; } if (this.screensaver != null && this.screensaver.enabled && this.screensaver.active) { return false; } return true; } private bool should_auto_advance () { if (!this._timer.is_finished () || this._current_time_block == null || this._current_time_block.state != Ft.State.POMODORO || this.next_time_block == null || !this.next_time_block.state.is_break ()) { return false; } if (this.lockscreen != null && this.lockscreen.enabled && this.lockscreen.active) { return true; } if (this.screensaver != null && this.screensaver.enabled && this.screensaver.active) { return true; } return false; } private void pause () { if (!this.auto_paused) { this.auto_paused = true; Ft.Context.set_event_source ("session-manager.auto-pause"); this._timer.pause (); } } private void resume () { if (this.auto_paused) { this.auto_paused = false; Ft.Context.set_event_source ("session-manager.auto-resume"); this._timer.resume (); } } private void enable_auto_pause () { if (this.lockscreen == null) { this.lockscreen = new Ft.LockScreen (); this.lockscreen.notify["active"].connect (this.on_lockscreen_notify_active); } if (this.screensaver == null) { this.screensaver = new Ft.ScreenSaver (); this.screensaver.notify["active"].connect (this.on_screensaver_notify_active); } if (this.should_auto_pause ()) { this.pause (); } } private void disable_auto_pause () { if (this.lockscreen != null) { this.lockscreen.notify["active"].disconnect (this.on_lockscreen_notify_active); this.lockscreen = null; } if (this.screensaver != null) { this.screensaver.notify["active"].disconnect (this.on_screensaver_notify_active); this.screensaver = null; } this.resume (); } private void update_auto_pause () { if (this._current_time_block != null && this.settings.get_boolean ("pause-on-lockscreen")) { this.enable_auto_pause (); } else { this.disable_auto_pause (); } } private void update_time_block_meta (Ft.TimeBlock time_block) { var completion_time = this._scheduler.calculate_time_block_completion_time (current_time_block); current_time_block.set_completion_time (completion_time); var weight = this._scheduler.calculate_time_block_weight (current_time_block); current_time_block.set_weight (weight); } /** * Update time-block according to changes in timer state. */ private void update_current_time_block (Ft.TimerState current_state, Ft.TimerState previous_state, int64 timestamp) requires (current_state.user_data == this._current_time_block) requires (this._current_time_block != null) requires (Ft.Timestamp.is_defined (timestamp)) { var previous_time_block = this.previous_time_block; var current_time_block = this._current_time_block; this.freeze_current_session_changed (); // Handle waiting for an activity. if (current_state.user_data == previous_state.user_data && current_state.is_started () && !previous_state.is_started ()) { var overdue = current_state.started_time - previous_time_block.end_time; if (previous_time_block != null && overdue < OVERDUE_TIMEOUT) { this.mark_time_block_end (previous_time_block, timestamp); } current_time_block.move_to (current_state.started_time); } // Handle duration change. current_time_block.end_time = current_state.is_started () ? current_state.started_time + current_state.offset + current_state.duration : current_time_block.start_time + current_state.offset + current_state.duration; if (current_state.user_data == previous_state.user_data && current_state.paused_time == previous_state.paused_time && current_state.offset != previous_state.offset) { // Handle rewind. var interval = current_state.offset - previous_state.offset; var gap_flags = current_time_block.state == Ft.State.POMODORO ? Ft.GapFlags.INTERRUPTION : Ft.GapFlags.DEFAULT; var gap = new Ft.Gap (gap_flags); gap.end_time = this._current_gap != null ? this._current_gap.start_time : timestamp; gap.start_time = int64.max ( Ft.Timestamp.subtract_interval (gap.end_time, interval), current_time_block.start_time); current_time_block.add_gap (gap); current_time_block.normalize_gaps (); } else if (current_state.is_paused ()) { // Mark pause start. if (this._current_gap == null) { var event_source = Ft.Context.get_event_source (); var gap_flags = current_time_block.state == Ft.State.POMODORO && ( event_source == "timer.pause" || event_source == "session-manager.auto-pause") ? Ft.GapFlags.INTERRUPTION : Ft.GapFlags.DEFAULT; this._current_gap = new Ft.Gap.with_start_time (current_state.paused_time, gap_flags); this._current_time_block.add_gap (this._current_gap); } else { if (this._current_gap.start_time != current_state.paused_time) { GLib.warning ("`Gap.start_time` does not match `TimerState.paused_time`."); } } } else { // Mark pause end. if (this._current_gap != null) { this.mark_gap_end (this._current_gap, timestamp); this._current_gap = null; } } this.update_time_block_meta (current_time_block); } /** * Update resolving timer state according to current session. * * Start a session or reschedule existing in order to construct a final timer state. */ private void resolve_timer_state (ref Ft.TimerState state, int64 timestamp) ensures (state.user_data == this._current_time_block) ensures (this.current_time_block_entered == (this._current_time_block != null)) { // Timer is paused or has finished. Nothing to resolve. // Advancing to a next time-block is handled after emitting `Timer.state_changed`. if (state.is_finished () || state.is_paused ()) { return; } // Waiting for activity. // `SessionManager` already set up everything in place. Update the timer state. if (!state.is_started () && state.user_data == this._current_time_block && this.active_watch_id != 0) { return; } // Stopping (resetting) the timer. // Adjust state as if the timer has already stopped. Handling will be continued in `Timer.state_changed`. if (!state.is_started () && state.user_data == null) { if (this._current_time_block != null) { this.advance_to_time_block (null, timestamp); this.initialize_timer_state (ref state, timestamp); } return; } // Timer is started by a session manager. if (state.user_data == this._current_time_block && this._current_time_block != null) { return; } // Start the timer with a new session. if (this._current_session != null && this._current_session.is_expired (timestamp)) { this.advance (state.started_time); this.initialize_timer_state (ref state, timestamp); return; } // Starting the timer. if (this._current_time_block == null) { this.advance_to_state (Ft.State.POMODORO, state.started_time); this.initialize_timer_state (ref state, timestamp); return; } } private void on_timer_resolve_state (ref Ft.TimerState state, int64 timestamp) requires (state.user_data == null || state.user_data == this._current_time_block) { this.resolving_timer_state++; this.resolve_timer_state (ref state, timestamp); if (this._current_time_block != null) { this.update_current_time_block (state, this._timer.state, timestamp); } this.resolving_timer_state--; } /** * React to timer state changes. */ private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { // Remove activity watch upon first change. if (this.active_watch_id != 0 && (current_state.is_started () || current_state.user_data == null)) { this.idle_monitor.remove_watch (this.active_watch_id); this.active_watch_id = 0; } // HACK: Use `resolving_timer_state` to preserve original timestamp // in `on_current_session_notify_expiry_time()`. this.resolving_timer_state++; this.bump_expiry_time (SESSION_EXPIRY_TIMEOUT); this.update_auto_pause (); this.reschedule_if_queued (); this.thaw_current_session_changed (); this.resolving_timer_state--; } private void on_timer_finished (Ft.TimerState state) { this.freeze_current_session_changed (); this.enable_idle_monitor (); var timestamp = state.finished_time; var next_time_block = this.initialize_next_time_block (timestamp); var advancement_mode = this.resolve_advancement_mode (next_time_block.state, timestamp); switch (advancement_mode) { case Ft.AdvancementMode.CONTINUOUS: if (this.active_watch_id != 0) { this.idle_monitor.remove_watch (this.active_watch_id); this.active_watch_id = 0; } this.advance_to_time_block (next_time_block, timestamp); break; case Ft.AdvancementMode.WAIT_FOR_ACTIVITY: // Jump to a next time-block and start the timer after detecting user activity. // It has only one use case: after a break, when we're certain we want to start pomodoro next. // `active_watch_id != 0` will indicate that we're waiting for activity. if (this.active_watch_id == 0) { this.active_watch_id = this.idle_monitor.add_active_watch (this.on_became_active); } this.advance_to_time_block (next_time_block, timestamp); break; case Ft.AdvancementMode.CONFIRM: // Keep the current time-block and let the timer indicate it has finished // until user confirms the advancement. Use it if you're uncertain whether the current state // should be extended. if (this.active_watch_id != 0) { this.idle_monitor.remove_watch (this.active_watch_id); this.active_watch_id = 0; } this.confirm_advancement (this._current_time_block, next_time_block); break; default: assert_not_reached (); } } private void on_timer_suspending (int64 start_time) { if (this._current_time_block != null) { this.pause (); // Splitting a gap into smaller chunks would complicate tracking the number of // interruptions, so flag ongoing gap as "sleep". var gap_flags = Ft.GapFlags.SLEEP; if (this._current_time_block.state == Ft.State.POMODORO && this._timer.is_running ()) { gap_flags |= Ft.GapFlags.INTERRUPTION; } if (this._current_gap == null) { this._current_gap = new Ft.Gap.with_start_time (start_time, gap_flags); this._current_time_block.add_gap (this._current_gap); } else { this._current_gap.set_flag (gap_flags); } } if (this._current_session != null) { GLib.SignalHandler.block (this._current_session, this.current_session_notify_expiry_time_id); this._current_session.expiry_time = start_time + SESSION_EXPIRY_TIMEOUT; GLib.SignalHandler.unblock (this._current_session, this.current_session_notify_expiry_time_id); } if (this.expiry_timeout_id != 0) { GLib.Source.remove (this.expiry_timeout_id); this.expiry_timeout_id = 0; } } private void on_timer_suspended (int64 start_time, int64 end_time) { if (this._current_session == null) { return; } if (this._current_session.is_expired (end_time)) { this.expire_current_session (end_time); } else { this.bump_expiry_time (SESSION_EXPIRY_TIMEOUT); } if (this._current_gap != null && this._current_gap.start_time == start_time && this._current_gap.has_flag (Ft.GapFlags.SLEEP) && !this.auto_paused) { this.mark_gap_end (this._current_gap, end_time); this._current_gap = null; } } private void on_current_time_block_changed (Ft.TimeBlock time_block) { if (this.resolving_timer_state == 0) { this.reschedule (); } else { this.queue_reschedule (); } if (this._current_state != time_block.state) { this._current_state = time_block.state; this.notify_property ("current-state"); } } private void on_scheduler_notify_session_template () { this.update_has_uniform_breaks (); this.queue_reschedule (); } private void handle_became_active () { if (this._timer.user_data == null || this._timer.is_started ()) { return; } if ((this.screensaver != null && this.screensaver.active) || (this.lockscreen != null && this.lockscreen.active)) { return; } this._timer.start (); } private void on_lockscreen_notify_active (GLib.Object object, GLib.ParamSpec pspec) { if (this.should_auto_pause ()) { this.pause (); } else if (this.should_auto_resume ()) { this.resume (); } else if (this.should_auto_advance ()) { this.advance (); } if (!this.lockscreen.active) { this.handle_became_active (); } } private void on_screensaver_notify_active (GLib.Object object, GLib.ParamSpec pspec) { if (this.should_auto_pause ()) { this.pause (); } else if (this.should_auto_resume ()) { this.resume (); } else if (this.should_auto_advance ()) { this.advance (); } if (!this.screensaver.active) { this.handle_became_active (); } } private void on_became_active () { this.active_watch_id = 0; this.handle_became_active (); } private void on_timezone_changed () { this.mark_timezone (this.timezone_monitor.timezone); } /** * A wrapper for `Timeout.add_seconds`. * * We don't want expiry callback to increment the `SessionManager` reference counter, * hence the static method and the use of pointer. */ private static uint setup_expiry_timeout (uint seconds, void* session_manager_ptr) { weak Ft.SessionManager session_manager = (Ft.SessionManager) session_manager_ptr; var timeout_id = GLib.Timeout.add_seconds ( seconds, () => { session_manager.expiry_timeout_id = 0; session_manager.expire_current_session (session_manager.current_session.expiry_time); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (timeout_id, "Ft.SessionManager.expire_current_session"); return timeout_id; } private void on_current_session_notify_expiry_time () { var current_time = this.get_current_time (); if (this.expiry_timeout_id != 0) { GLib.Source.remove (this.expiry_timeout_id); this.expiry_timeout_id = 0; } if (this._current_session.expiry_time > current_time) { var timeout_seconds = Ft.Timestamp.to_seconds_uint ( Ft.Timestamp.round_seconds (this._current_session.expiry_time - current_time)); this.expiry_timeout_id = Ft.SessionManager.setup_expiry_timeout (timeout_seconds, this); } } private void on_settings_changed (string key) { switch (key) { case "pomodoro-duration": case "short-break-duration": case "long-break-duration": case "cycles": this.update_session_template (); break; case "pause-on-lockscreen": this.update_auto_pause (); break; case "pomodoro-advancement-mode": case "break-advancement-mode": break; } } private void enable_idle_monitor () { if (this.idle_monitor == null) { this.idle_monitor = new Ft.IdleMonitor (); } } private void disable_idle_monitor () { if (this.active_watch_id != 0) { this.idle_monitor.remove_watch (this.active_watch_id); this.active_watch_id = 0; } this.idle_monitor = null; } /** * Initialize session if there is no current session or if it expired. */ public void ensure_session (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); if (this._current_session == null || this._current_session.is_expired (timestamp)) { var next_session = this.initialize_next_session (timestamp); this.set_current_time_block_full (next_session, null, timestamp); } } public bool can_reset () { var current_session = this._current_session; var is_waiting_for_activity = !this._timer.is_started () && this._timer.user_data != null; return current_session != null ? !current_session.is_scheduled () && !is_waiting_for_activity : false; } /** * Start a new session. The `timestamp` marks the end time of ongoing session. */ public void reset (int64 timestamp = Ft.Timestamp.UNDEFINED) { var now = Ft.Timestamp.from_now (); if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = now; } if (this._current_session != null && !this._current_session.is_scheduled ()) { var next_session = this.initialize_next_session (now); this.set_current_time_block_full (next_session, null, timestamp); } } /** * Check and expire session if needed. Used in tests. */ public void check_current_session_expired () { var timestamp = Ft.Timestamp.from_now (); if (this._current_session != null && this._current_session.is_expired (timestamp)) { this.expire_current_session (timestamp); } } /** * Session is entered as soon as current-session property is set. */ [Signal (run = "first")] public signal void enter_session (Ft.Session session) { assert (session == this._current_session); this.current_session_entered = true; this.current_session_notify_expiry_time_id = this._current_session.notify["expiry-time"].connect ( this.on_current_session_notify_expiry_time); this.update_has_uniform_breaks (); this.emit_session_rescheduled (); } [Signal (run = "first")] public signal void leave_session (Ft.Session session) { assert (session == this._current_session); this.thaw_current_session_changed (); session.disconnect (this.current_session_notify_expiry_time_id); this.current_session_entered = false; this.current_session_notify_expiry_time_id = 0; } /** * Time block is entered as soon as current-time-block property is set. * You should check timer whether time block has really started. */ [Signal (run = "first")] public signal void enter_time_block (Ft.TimeBlock time_block) { assert (time_block == this._current_time_block); assert (time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED || time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); if (this.current_time_block_changed_id != 0) { GLib.warning ("`TimeBlock.changed` signal handler has not been disconnected properly"); } this.current_time_block_entered = true; this.current_time_block_changed_id = time_block.changed.connect (this.on_current_time_block_changed); this.next_session = null; this.next_time_block = null; time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); } [Signal (run = "first")] public signal void leave_time_block (Ft.TimeBlock time_block) { assert (time_block == this._current_time_block); assert (time_block.get_status () == Ft.TimeBlockStatus.COMPLETED || time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); this.current_time_block_entered = false; if (this.current_time_block_changed_id != 0) { time_block.disconnect (this.current_time_block_changed_id); this.current_time_block_changed_id = 0; } } public signal void session_rescheduled (Ft.Session session, int64 timestamp) { if (this.current_session_changed_frozen && Ft.is_test ()) { GLib.debug ("Rescheduled session while setting current-time-block"); } } public signal void session_expired (Ft.Session session, int64 timestamp) { GLib.debug ("Session expired"); } /** * Outward signal that we need a confirmation before advancing to a next time-block. * Until then the timer will indicate that it has finished, the session-manager will keep current time-block * intact. You should use skip action or `.advance()` to jump to the next time-block. */ public signal void confirm_advancement (Ft.TimeBlock current_time_block, Ft.TimeBlock next_time_block); /** * A convenience signal to handle transition between time-blocks. * * It's emitted after entering the current_time_block. */ public signal void advanced (Ft.Session? current_session, Ft.TimeBlock? current_time_block, Ft.Session? previous_session, Ft.TimeBlock? previous_time_block); public signal void time_block_saved (Ft.TimeBlock time_block, Ft.TimeBlockEntry time_block_entry); public signal void gap_saved (Ft.Gap gap, Ft.GapEntry gap_entry); public override void dispose () { if (this._timer != null) { if (this.timer_resolve_state_id != 0) { this._timer.disconnect (this.timer_resolve_state_id); this.timer_resolve_state_id = 0; } if (this.timer_state_changed_id != 0) { this._timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } if (this.timer_finished_id != 0) { this._timer.disconnect (this.timer_finished_id); this.timer_finished_id = 0; } if (this.timer_suspending_id != 0) { this._timer.disconnect (this.timer_suspending_id); this.timer_suspending_id = 0; } if (this.timer_suspended_id != 0) { this._timer.disconnect (this.timer_suspended_id); this.timer_suspended_id = 0; } } if (this._scheduler != null) { this._scheduler.notify["session-template"].disconnect ( this.on_scheduler_notify_session_template); } if (this.settings != null) { this.settings.changed.disconnect (this.on_settings_changed); } if (this.reschedule_idle_id != 0) { GLib.Source.remove (this.reschedule_idle_id); this.reschedule_idle_id = 0; } if (this.session_changed_idle_id != 0) { GLib.Source.remove (this.session_changed_idle_id); this.session_changed_idle_id = 0; } if (this.expiry_timeout_id != 0) { GLib.Source.remove (this.expiry_timeout_id); this.expiry_timeout_id = 0; } this.disable_auto_pause (); this.disable_idle_monitor (); this._current_gap = null; this._current_time_block = null; this._current_session = null; this._timer = null; this._scheduler = null; this.next_time_block = null; this.next_session = null; this.idle_monitor = null; this.timezone_monitor = null; this.timezone_history = null; this.lockscreen = null; this.screensaver = null; this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/session.vala000066400000000000000000000665351520625676500227450ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public struct SessionTemplate { public int64 pomodoro_duration; public int64 short_break_duration; public int64 long_break_duration; public uint cycles; public SessionTemplate.with_defaults () { var settings = Ft.get_settings (); this.pomodoro_duration = Ft.Timestamp.from_seconds_uint ( settings.get_uint ("pomodoro-duration") ); this.short_break_duration = Ft.Timestamp.from_seconds_uint ( settings.get_uint ("short-break-duration") ); this.long_break_duration = Ft.Timestamp.from_seconds_uint ( settings.get_uint ("long-break-duration") ); this.cycles = settings.get_uint ("cycles"); } public bool equals (Ft.SessionTemplate other) { return this.pomodoro_duration == other.pomodoro_duration && this.short_break_duration == other.short_break_duration && this.long_break_duration == other.long_break_duration && this.cycles == other.cycles; } public int64 calculate_total_duration () { var total_duration = this.pomodoro_duration * this.cycles; total_duration += cycles > 1 ? this.short_break_duration * (this.cycles - 1) + this.long_break_duration : this.short_break_duration; return total_duration; } /** * Calculate percentage of time allocated for breaks compared to total. * * Result is in 0 - 100 range. */ public double calculate_break_percentage () { var breaks_duration = this.cycles > 1 ? this.short_break_duration * (this.cycles - 1) + this.long_break_duration : this.short_break_duration; var total_duration = this.pomodoro_duration * this.cycles + breaks_duration; var ratio = total_duration > 0 ? 100.0 * (double) breaks_duration / (double) total_duration : 0.0; return ratio; } public bool has_uniform_breaks () { return this.cycles == 1; } public int64 get_duration (Ft.State state) { switch (state) { case Ft.State.POMODORO: return this.pomodoro_duration; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: return this.short_break_duration; case Ft.State.LONG_BREAK: return this.long_break_duration; case Ft.State.STOPPED: return 0; default: assert_not_reached (); } } public GLib.Variant to_variant () { var builder = new GLib.VariantBuilder (new GLib.VariantType ("a{s*}")); builder.add ("{sv}", "pomodoro_duration", new GLib.Variant.int64 (this.pomodoro_duration)); builder.add ("{sv}", "short_break_duration", new GLib.Variant.int64 (this.short_break_duration)); builder.add ("{sv}", "long_break_duration", new GLib.Variant.int64 (this.long_break_duration)); builder.add ("{sv}", "cycles", new GLib.Variant.uint32 (this.cycles)); return builder.end (); } /** * Represent template as string. * * Used in tests. */ public string to_representation () { var representation = new GLib.StringBuilder ("SessionTemplate (\n"); representation.append (@" pomodoro_duration = $pomodoro_duration,\n"); representation.append (@" short_break_duration = $short_break_duration,\n"); representation.append (@" long_break_duration = $long_break_duration,\n"); representation.append (@" cycles = $cycles\n"); representation.append (")"); return representation.str; } } /** * Ft.Session class. * * By "session" in this project we mean mostly a streak of pomodoros interleaved with breaks. * Session ends with either by a long break or inactivity. * * Class acts as a container. It merely defines a group of time blocks. It can be used to represent * historic sessions. */ public class Session : GLib.Object { public int64 start_time { get { return this._start_time; } } public int64 end_time { get { return this._end_time; } } /** * Duration of a session. * * It will include interruptions / gap times. For real time spent use `calculate_elapsed()`. */ public int64 duration { get { return Ft.Timestamp.subtract (this._end_time, this._start_time); } } /** * Manually set expiry time. * * A session can't determine whether it expired on its own in some circumstances. * For instance it's not aware when the timer gets paused, so the responsibility of managing expiry * passes on to a session manager. */ [CCode (notify = false)] public int64 expiry_time { get { return this._expiry_time; } set { if (this._expiry_time == value) { return; } this._expiry_time = value; this.version++; this.notify_property ("expiry-time"); } } /** * Time-blocks can me modified by scheduler. */ internal GLib.List time_blocks; // XXX: make it private internal ulong version = 0; internal Ft.SessionEntry? entry = null; private int64 _start_time = Ft.Timestamp.UNDEFINED; private int64 _end_time = Ft.Timestamp.UNDEFINED; private int64 _expiry_time = Ft.Timestamp.UNDEFINED; private GLib.List cycles; private bool cycles_need_update = true; private int changed_freeze_count = 0; private bool changed_is_pending = false; /** * Create empty session. */ public Session () { } /** * Create session with according to given template. * * Does not take into account users schedule. * * It's intended to be used in unit tests. In real world, session should be built by `Scheduler`. */ public Session.from_template (Ft.SessionTemplate template, int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); this.freeze_changed (); this._start_time = timestamp; for (var cycle = 1U; cycle <= template.cycles; cycle++) { var needs_long_break = cycle >= template.cycles; var pomodoro_time_block = new Ft.TimeBlock.with_start_time ( timestamp, Ft.State.POMODORO); pomodoro_time_block.duration = template.pomodoro_duration; this.time_blocks.append (pomodoro_time_block); this.emit_added (pomodoro_time_block); timestamp += pomodoro_time_block.duration; this._end_time = timestamp; var break_time_block = new Ft.TimeBlock.with_start_time ( timestamp, needs_long_break ? Ft.State.LONG_BREAK : Ft.State.SHORT_BREAK); break_time_block.duration = needs_long_break ? template.long_break_duration : template.short_break_duration; this.time_blocks.append (break_time_block); this.emit_added (break_time_block); timestamp += break_time_block.duration; this._end_time = timestamp; } this.thaw_changed (); } private void update_time_range () { unowned var first_time_block = this.get_first_time_block (); unowned var last_time_block = this.get_last_time_block (); var old_duration = this._end_time - this._start_time; var start_time = first_time_block != null ? first_time_block.start_time : Ft.Timestamp.UNDEFINED; var end_time = last_time_block != null ? last_time_block.end_time : Ft.Timestamp.UNDEFINED; if (this._start_time != start_time) { this._start_time = start_time; this.notify_property ("start-time"); } if (this._end_time != end_time) { this._end_time = end_time; this.notify_property ("end-time"); } if (this._end_time - this._start_time != old_duration) { this.notify_property ("duration"); } } internal void invalidate_cycles () { this.cycles_need_update = true; } private void update_cycles () { var cycles = new GLib.List (); unowned Ft.Cycle? last_cycle = null; unowned GLib.List link = this.time_blocks.first (); while (link != null) { if (last_cycle == null || link.data.state == Ft.State.POMODORO) { var cycle = new Ft.Cycle (); cycles.append (cycle); last_cycle = cycle; } last_cycle.append (link.data); link = link.next; } this.cycles = (owned) cycles; this.cycles_need_update = false; } private void update_cycles_if_queued () { if (this.cycles_need_update) { this.update_cycles (); } } internal void emit_added (Ft.TimeBlock time_block) { time_block.session = this; time_block.changed.connect (this.on_time_block_changed); this.added (time_block); } internal void emit_removed (Ft.TimeBlock time_block) { time_block.session = null; time_block.changed.disconnect (this.on_time_block_changed); this.removed (time_block); } private void emit_changed () { this.version++; if (this.changed_freeze_count > 0) { this.changed_is_pending = true; } else { this.changed_is_pending = false; this.changed (); } } public void freeze_changed () { this.changed_freeze_count++; } public void thaw_changed () { this.changed_freeze_count--; if (this.changed_freeze_count == 0 && this.changed_is_pending) { this.emit_changed (); } } private void on_time_block_changed () { this.emit_changed (); } /* * Methods for managing session as a whole */ /** * Check whether a session is suitable for reuse after being unused. */ public bool is_expired (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); return this._expiry_time >= 0 ? timestamp >= this._expiry_time : false; } public bool is_scheduled () { unowned GLib.List link = this.time_blocks.first (); var first_status = link != null ? link.data.get_status () : Ft.TimeBlockStatus.SCHEDULED; return first_status == Ft.TimeBlockStatus.SCHEDULED; } /** * Return whether session has a completed long break. * * The time-block must be marked with proper status; in-progress status won't do. */ public bool is_completed () { unowned GLib.List link = this.time_blocks.first (); while (link != null) { if (link.data.get_status () == Ft.TimeBlockStatus.COMPLETED && ( link.data.state == Ft.State.LONG_BREAK || link.data.state == Ft.State.BREAK)) { return true; } link = link.next; } return false; } /** * Calculate ratio between time elapsed on breaks and total elapsed time. * * The intention here is to have a percentage how much of the time is spent on breaks. */ public float calculate_break_ratio (int64 timestamp = -1) { Ft.ensure_timestamp (ref timestamp); unowned GLib.List link = this.time_blocks.first (); int64 pomodoros_total = 0; int64 breaks_total = 0; while (link != null) { var time_block = link.data; switch (time_block.state) { case Ft.State.POMODORO: pomodoros_total = Ft.Interval.add (pomodoros_total, time_block.calculate_elapsed (timestamp)); break; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: breaks_total = Ft.Interval.add (breaks_total, time_block.calculate_elapsed (timestamp)); break; default: assert_not_reached (); } link = link.next; } var total = Ft.Interval.add (pomodoros_total, breaks_total); var ratio = total > 0 ? (double) breaks_total / (double) total : 0.0; return (float) ratio; } /* * Methods for container operations */ /** * Remove link. */ internal void remove_link (GLib.List? link) { if (link == null) { return; } var time_block = link.data; time_block.session = null; link.data = null; this.time_blocks.delete_link (link); this.emit_removed (time_block); } /** * Remove links following. */ internal void remove_links_after (GLib.List? link) { if (link == null) { return; } if (link.next != null) { this.remove_links_after (link.next); this.remove_link (link.next); } } /** * Add the time-block and align it with the last block. */ public void append (Ft.TimeBlock time_block) requires (time_block.session == null) { var last_time_block = this.get_last_time_block (); if (last_time_block != null) { time_block.move_to (last_time_block.end_time); } this.time_blocks.append (time_block); this.emit_added (time_block); } /** * Add the time-block as first and align it to first time-block. */ public void prepend (Ft.TimeBlock time_block) requires (time_block.session == null) { var first_time_block = this.get_first_time_block (); if (first_time_block != null) { var new_start_time = Ft.Timestamp.subtract (first_time_block.start_time, time_block.duration); time_block.move_to (new_start_time); } this.time_blocks.prepend (time_block); this.emit_added (time_block); } /** * Insert the time-block before given time-block. */ public void insert_before (Ft.TimeBlock time_block, Ft.TimeBlock sibling) requires (time_block.session == null) requires (sibling.session == this) { unowned GLib.List sibling_link = this.time_blocks.find (sibling); time_block.move_to (sibling.start_time); this.time_blocks.insert_before (sibling_link, time_block); this.emit_added (time_block); } /** * Insert the time-block after given time-block and align it to the sibling. */ public void insert_after (Ft.TimeBlock time_block, Ft.TimeBlock sibling) requires (time_block.session == null) requires (sibling.session == this) { unowned GLib.List sibling_link = this.time_blocks.find (sibling); if (sibling_link.next == null) { this.append (time_block); } else { time_block.move_to (sibling.end_time); this.time_blocks.insert_before (sibling_link.next, time_block); this.emit_added (time_block); } } public void remove (Ft.TimeBlock time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link != null) { this.remove_link (link); } else { GLib.warning ("Ignoring `Session.remove()`. Time-block does not belong to the session."); } } public void remove_before (Ft.TimeBlock time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link == null) { GLib.warning ("Ignoring `Session.remove_before()`. Time-block does not belong to the session."); return; } this.freeze_changed (); while (link.prev != null) { this.remove_link (link.prev); } this.thaw_changed (); } public void remove_after (Ft.TimeBlock time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link == null) { GLib.warning ("Ignoring `Session.remove_after()`. Time-block does not belong to the session."); return; } this.freeze_changed (); while (link.next != null) { this.remove_link (link.next); } this.thaw_changed (); } public void remove_scheduled () { unowned GLib.List link = this.time_blocks.first (); unowned GLib.List tmp; this.freeze_changed (); while (link != null) { if (link.data.get_status () == Ft.TimeBlockStatus.SCHEDULED) { tmp = link.next; this.remove_link (link); link = tmp; } else { link = link.next; } } this.thaw_changed (); } public unowned Ft.TimeBlock? get_first_time_block () { unowned GLib.List link = this.time_blocks.first (); return link != null ? link.data : null; } public unowned Ft.TimeBlock? get_last_time_block () { unowned GLib.List link = this.time_blocks.last (); return link != null ? link.data : null; } public unowned Ft.TimeBlock? get_nth_time_block (uint index) { return this.time_blocks.nth_data (index); } public unowned Ft.TimeBlock? get_previous_time_block (Ft.TimeBlock? time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link == null || link.prev == null) { return null; } return link.prev.data; } public unowned Ft.TimeBlock? get_next_time_block (Ft.TimeBlock? time_block) { unowned GLib.List link = this.time_blocks.find (time_block); if (link == null || link.next == null) { return null; } return link.next.data; } public int index (Ft.TimeBlock time_block) { return this.time_blocks.index (time_block); } public bool contains (Ft.TimeBlock time_block) { return this.time_blocks.index (time_block) >= 0; } public void @foreach (GLib.Func func) { this.time_blocks.@foreach (func); } public void move_by (int64 offset) { var logged_warnining = false; this.freeze_changed (); this.time_blocks.@foreach ((time_block) => { if (!logged_warnining && time_block.get_status () > Ft.TimeBlockStatus.IN_PROGRESS) { GLib.warning ("Moving time-blocks that have been completed."); logged_warnining = true; } time_block.move_by (offset); }); this.thaw_changed (); } /** * Move all time blocks to a given timestamp, even started / completed. * * You manually modify time blocks. Consider calling .reschedule() . */ public void move_to (int64 timestamp) { unowned GLib.List link = this.time_blocks.first (); if (link != null) { this.move_by (Ft.Timestamp.subtract (timestamp, link.data.start_time)); } } /* * Methods for managing time-blocks metadata */ public void set_time_block_status (Ft.TimeBlock time_block, Ft.TimeBlockStatus status) { var changed = false; unowned GLib.List link = this.time_blocks.first (); if (!this.contains (time_block)) { return; } while (link != null) { var prevoious_status = link.data.get_status (); if (link.data == time_block) { if (prevoious_status != status) { link.data.set_status (status); changed = true; } break; } if (prevoious_status <= Ft.TimeBlockStatus.IN_PROGRESS) { link.data.set_status (Ft.TimeBlockStatus.UNCOMPLETED); changed = true; } link = link.next; } if (changed) { this.emit_changed (); } } /** * Cycles are not an structural part of a session, they are more like annotations. They are generated if needed * with `get_cycles ()`. */ public GLib.List get_cycles () { this.update_cycles_if_queued (); return this.cycles.copy (); } public uint count_time_blocks (Ft.FilterFunc? func = null) { if (func == null) { return this.time_blocks.length (); } unowned GLib.List link = this.time_blocks.first (); var count = 0U; while (link != null) { if (func (link.data)) { count++; } link = link.next; } return count; } public uint count_cycles (Ft.FilterFunc? func = null) { this.update_cycles_if_queued (); if (func == null) { return this.cycles.length (); } unowned GLib.List link = this.cycles.first (); var count = 0U; while (link != null) { if (func (link.data)) { count++; } link = link.next; } return count; } public inline uint count_visible_cycles () { return this.count_cycles ((cycle) => cycle.is_visible ()); } /* * Databaase */ internal bool should_create_entry () { unowned GLib.List link = this.time_blocks.first (); while (link != null) { if (link.data.should_create_entry ()) { return true; } link = link.next; } return false; } internal bool should_update_entry () { if (this.entry == null || this.entry.id == 0) { return true; } return this.entry.version != this.version; } internal unowned Ft.SessionEntry create_or_update_entry () { if (this.entry == null) { this.entry = new Ft.SessionEntry (); this.entry.repository = Ft.Database.get_repository (); } this.entry.start_time = this._start_time; this.entry.end_time = this._end_time; this.entry.expiry_time = this._expiry_time; this.entry.version = this.version; return this.entry; } internal void unset_entry () { unowned GLib.List link = this.time_blocks.first (); while (link != null) { link.data.unset_entry (); link = link.next; } this.entry = null; } /* * Signals */ [Signal (run = "last")] public signal void added (Ft.TimeBlock time_block) { this.emit_changed (); } [Signal (run = "last")] public signal void removed (Ft.TimeBlock time_block) { this.emit_changed (); } [Signal (run = "first")] public signal void changed () { this.update_time_range (); this.invalidate_cycles (); } public override void dispose () { this.time_blocks = null; this.entry = null; this.cycles = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/settings.vala000066400000000000000000000010661520625676500231060ustar00rootroot00000000000000/* * Copyright (c) 2014-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { private GLib.Settings settings = null; public void set_settings (GLib.Settings settings) { Ft.settings = settings; } public unowned GLib.Settings get_settings () { if (Ft.settings == null) { Ft.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer"); // TODO: unset Pomodoro.settings at application exit } return Ft.settings; } } focustimerhq-FocusTimer-8581be2/src/core/sleep-monitor.vala000066400000000000000000000024111520625676500240360ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface SleepMonitorProvider : Ft.Provider { public signal void prepare_for_sleep (); public signal void woke_up (); } [SingleInstance] public class SleepMonitor : Ft.ProvidedObject { private void on_prepare_for_sleep () { this.prepare_for_sleep (); } private void on_woke_up () { this.woke_up (); } protected override void initialize () { } protected override void setup_providers () { } protected override void provider_enabled (Ft.SleepMonitorProvider provider) { provider.prepare_for_sleep.connect (this.on_prepare_for_sleep); provider.woke_up.connect (this.on_woke_up); } protected override void provider_disabled (Ft.SleepMonitorProvider provider) { provider.prepare_for_sleep.disconnect (this.on_prepare_for_sleep); provider.woke_up.disconnect (this.on_woke_up); } public signal void prepare_for_sleep (); public signal void woke_up (); } } focustimerhq-FocusTimer-8581be2/src/core/sound-manager.vala000066400000000000000000000171211520625676500240050ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { [SingleInstance] public class SoundManager : GLib.Object { private const int64 SHORT_FADE_DURATION = 300 * Ft.Interval.MILLISECOND; private const int64 LONG_FADE_DURATION = Ft.Interval.SECOND; // Silence at the end of Pomodoro serves as a cue that it's about to end private const int64 ABOUT_TO_END_TIME = 10 * Ft.Interval.SECOND; private Ft.AlertSound? pomodoro_finished_sound = null; private Ft.AlertSound? break_finished_sound = null; private Ft.BackgroundSound? background_sound = null; private uint background_sound_inhibit_count = 0; private GLib.Settings? settings = null; private Ft.Timer? timer = null; private ulong timer_state_changed_id = 0; private uint fade_out_timeout_id = 0; construct { this.timer = Ft.Timer.get_default (); this.timer_state_changed_id = this.timer.state_changed.connect_after (this.on_timer_state_changed); this.pomodoro_finished_sound = new Ft.AlertSound ("pomodoro-finished"); this.break_finished_sound = new Ft.AlertSound ("break-finished"); this.background_sound = new Ft.BackgroundSound (); this.background_sound.loop = true; this.background_sound.fade_out (0); this.settings = Ft.get_settings (); this.settings.bind ("pomodoro-finished-sound", this.pomodoro_finished_sound, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("pomodoro-finished-sound-volume", this.pomodoro_finished_sound, "volume", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("break-finished-sound", this.break_finished_sound, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("break-finished-sound-volume", this.break_finished_sound, "volume", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("background-sound", this.background_sound, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("background-sound-volume", this.background_sound, "volume", GLib.SettingsBindFlags.DEFAULT); this.update_background_sound (); } private void schedule_fade_out (int64 timeout) requires (this.fade_out_timeout_id == 0) { this.fade_out_timeout_id = GLib.Timeout.add (Ft.Timestamp.to_milliseconds_uint (timeout), this.on_fade_out_timeout); GLib.Source.set_name_by_id (this.fade_out_timeout_id, "Ft.SoundManager.on_fade_out_timeout"); } private void unschedule_fade_out () { if (this.fade_out_timeout_id != 0) { GLib.Source.remove (this.fade_out_timeout_id); this.fade_out_timeout_id = 0; } } private void update_background_sound () { this.unschedule_fade_out (); if (this.background_sound_inhibit_count != 0) { this.background_sound.fade_out (LONG_FADE_DURATION); return; } if (!this.background_sound.can_play ()) { return; } var current_time_block = this.timer.user_data as Ft.TimeBlock; var current_state = current_time_block != null ? current_time_block.state : Ft.State.STOPPED; if (current_state == Ft.State.POMODORO && this.timer.is_running ()) { var remaining = this.timer.calculate_remaining (); var fade_duration = LONG_FADE_DURATION; if (remaining > ABOUT_TO_END_TIME) { this.background_sound.fade_in (fade_duration); this.schedule_fade_out (remaining - ABOUT_TO_END_TIME); } else { fade_duration = int64.max (remaining - Ft.Interval.SECOND, SHORT_FADE_DURATION); this.background_sound.fade_out (fade_duration); } } else { this.background_sound.fade_out (SHORT_FADE_DURATION); } } public void inhibit_background_sound () { this.background_sound_inhibit_count++; if (this.background_sound_inhibit_count == 1) { this.update_background_sound (); } } public void uninhibit_background_sound () { this.background_sound_inhibit_count--; if (this.background_sound_inhibit_count == 0) { this.update_background_sound (); } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { if (current_state.is_finished () && !previous_state.is_finished () && previous_state.user_data != null) { var current_time_block = current_state.user_data as Ft.TimeBlock; if (current_time_block.state == Ft.State.POMODORO) { this.pomodoro_finished_sound.play (); } else if (current_time_block.state.is_break ()) { this.break_finished_sound.play (); } } this.update_background_sound (); } private bool on_fade_out_timeout () requires (this.timer.is_running ()) { var current_time = this.timer.get_current_time (GLib.MainContext.current_source ().get_time ()); var remaining = this.timer.calculate_remaining (current_time); var fade_out_duration = int64.max (remaining - Ft.Interval.SECOND, SHORT_FADE_DURATION); this.fade_out_timeout_id = 0; this.background_sound.fade_out (fade_out_duration); return GLib.Source.REMOVE; } public override void dispose () { this.unschedule_fade_out (); if (this.timer_state_changed_id != 0) { this.timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } if (this.pomodoro_finished_sound != null) { this.pomodoro_finished_sound.stop (); this.pomodoro_finished_sound = null; } if (this.break_finished_sound != null) { this.break_finished_sound.stop (); this.break_finished_sound = null; } if (this.background_sound != null) { this.background_sound.stop (); this.background_sound = null; } this.timer = null; this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/sound-player.vala000066400000000000000000000413721520625676500236740ustar00rootroot00000000000000/* * Copyright (c) 2016,2024,2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [Flags] private enum GstPlayFlags { VIDEO = 0x00000001, AUDIO = 0x00000002, TEXT = 0x00000004, VIS = 0x00000008, SOFT_VOLUME = 0x00000010, NATIVE_AUDIO = 0x00000020, NATIVE_VIDEO = 0x00000040, DOWNLOAD = 0x00000080, BUFFERING = 0x00000100, DEINTERLACE = 0x00000200, SOFT_COLORBALANCE = 0x00000400, FORCE_FILTERS = 0x00000800 } public class SoundPlayer : GLib.Object { private const uint LATENCY = 200; // milliseconds public string uri { get { return this.pipeline.uri; } set { Gst.State state; Gst.State pending_state; this.pipeline.get_state (out state, out pending_state, Gst.CLOCK_TIME_NONE); if (pending_state != Gst.State.VOID_PENDING) { state = pending_state; } if (state == Gst.State.PLAYING) { this.pipeline.set_state (Gst.State.READY); this.pipeline.uri = value; this.pipeline.set_state (Gst.State.PLAYING); } else { this.pipeline.uri = value; this.pipeline.set_state (Gst.State.READY); } } } public double volume { get { return this.pipeline.volume; } set { this.pipeline.volume = value.clamp (0.0, 1.0); } } public bool loop { get; set; default = false; } private dynamic Gst.Element pipeline; private dynamic Gst.Element volume_filter; private dynamic Gst.Element audio_sink; private Gst.Controller.InterpolationControlSource? volume_interpolation; private bool _is_playing = false; private bool is_about_to_finish = false; private double fade_to = 0.0; private uint stop_timeout_id = 0; private uint park_timeout_id = 0; private static bool is_gstreamer_initialized = false; public SoundPlayer () throws GLib.Error { if (!is_gstreamer_initialized) { unowned string[] args_unowned = null; Gst.init (ref args_unowned); is_gstreamer_initialized = true; } dynamic Gst.Element pipeline = Gst.ElementFactory.make ("playbin", "player"); dynamic Gst.Element volume_filter = Gst.ElementFactory.make ("volume", "volume"); dynamic Gst.Element audio_sink = Gst.ElementFactory.make ("autoaudiosink", "audio-output"); if (pipeline == null || volume_filter == null || audio_sink == null) { throw new Ft.SoundError.NOT_INITIALIZED (_("Failed to initialize playback")); } pipeline.flags = GstPlayFlags.AUDIO; pipeline.audio_filter = volume_filter; pipeline.audio_sink = audio_sink; // Keep the audio sink locked at NULL when idle. This forces // autoaudiosink to re-probe for the current default device on each // play(), while the source/decoder stays warm. audio_sink.set_locked_state (true); pipeline.about_to_finish.connect (this.on_about_to_finish); pipeline.get_bus ().add_watch (GLib.Priority.DEFAULT, this.on_bus_callback); this.pipeline = pipeline; this.volume_filter = volume_filter; this.audio_sink = audio_sink; } public static string[] get_supported_mime_types () { return { "audio/*" }; } private void finished () { string current_uri; if (this.loop) { this.pipeline.@get ("current-uri", out current_uri); if (current_uri != "") { this.pipeline.@set ("uri", current_uri); } } } private void on_about_to_finish () { this.is_about_to_finish = true; this.finished (); if (this.loop && this.volume_interpolation != null) { var running_time = this.get_running_time (); var current_fade_value = this.get_fade_value (running_time); var control_points = new Gst.TimedValue[0]; this.volume_interpolation.get_all ().@foreach ( (control_point) => { if (control_point.timestamp > running_time) { control_points += Gst.TimedValue () { timestamp = control_point.timestamp - running_time, value = control_point.value, }; } }); if (control_points.length > 0) { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, current_fade_value); foreach (var control_point in control_points) { this.volume_interpolation.@set (control_point.timestamp, control_point.value); } } else { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, this.fade_to); } } } private bool on_bus_callback (Gst.Bus bus, Gst.Message message) { Gst.State state; Gst.State pending_state; GLib.Error? error = null; string? debug_info = null; var src_name = message.src != null ? message.src.name : ""; this.pipeline.get_state (out state, out pending_state, Gst.CLOCK_TIME_NONE); switch (message.type) { case Gst.MessageType.STATE_CHANGED: if (!this._is_playing && state == Gst.State.PLAYING) { this._is_playing = true; this.playback_started (); } if (this._is_playing && state != Gst.State.PLAYING) { this._is_playing = false; this.playback_stopped (); } break; case Gst.MessageType.EOS: if (this.is_about_to_finish) { this.is_about_to_finish = false; } else { this.finished (); } if (!this.loop && pending_state != Gst.State.PLAYING) { this.pipeline.set_state (Gst.State.READY); this.park_audio_sink (); } break; case Gst.MessageType.ERROR: if (this.is_about_to_finish) { this.is_about_to_finish = false; } message.parse_error (out error, out debug_info); GLib.warning ("SoundPlayer error from %s: %s [debug: %s]", src_name, error.message, debug_info ?? ""); this.pipeline.set_state (Gst.State.NULL); this.playback_error (error); break; case Gst.MessageType.WARNING: message.parse_warning (out error, out debug_info); GLib.warning ("SoundPlayer warning from %s: %s [debug: %s]", src_name, error.message, debug_info ?? ""); break; default: break; } return GLib.Source.CONTINUE; } public Gst.ClockTime get_running_time () { var running_time = (Gst.ClockTime) 0; pipeline.query_position (Gst.Format.TIME, out running_time); return running_time; } private void park_audio_sink () { if (this.park_timeout_id != 0) { GLib.Source.remove (this.park_timeout_id); this.park_timeout_id = 0; } this.audio_sink.set_locked_state (true); this.audio_sink.set_state (Gst.State.NULL); } private void ensure_volume_interpolation () { if (this.volume_interpolation != null) { return; } var volume_interpolation = new Gst.Controller.InterpolationControlSource (); volume_interpolation.mode = Gst.Controller.InterpolationMode.LINEAR; volume_interpolation.@set (0, 1.0); var binding = new Gst.Controller.DirectControlBinding.with_absolute ( this.volume_filter, "volume", volume_interpolation); this.volume_filter.add_control_binding (binding); this.volume_interpolation = volume_interpolation; } private double get_fade_value (Gst.ClockTime running_time) { double fade_value = double.NAN; if (running_time == Gst.CLOCK_TIME_NONE) { return this.fade_to; } if (this.volume_interpolation != null && this.volume_interpolation.get_value (running_time, out fade_value)) { return fade_value; } return this.fade_to; } private void unschedule_stop () { if (this.stop_timeout_id != 0) { GLib.Source.remove (this.stop_timeout_id); this.stop_timeout_id = 0; } } private bool on_stop_timeout () { this.stop_timeout_id = 0; this.stop (); return GLib.Source.REMOVE; } public void play () requires (this.pipeline != null) { if (this.park_timeout_id != 0) { GLib.Source.remove (this.park_timeout_id); this.park_timeout_id = 0; } if (this.uri != "") { this.audio_sink.set_locked_state (false); this.pipeline.set_state (Gst.State.PLAYING); } } public void stop () { Gst.State state; Gst.State pending_state; if (this.pipeline == null) { return; } this.pipeline.get_state (out state, out pending_state, Gst.CLOCK_TIME_NONE); if (pending_state != Gst.State.VOID_PENDING) { state = pending_state; } if (state != Gst.State.NULL && state != Gst.State.READY) { this.pipeline.set_state (Gst.State.READY); // set_state() is async; wait for READY before scheduling // the park. Without this wait the audio sink could still be // mid-transition when the deferred park fires. this.pipeline.get_state (out state, out pending_state, Gst.CLOCK_TIME_NONE); } // Defer the hardware teardown to let the PulseAudio DMA buffer // drain to silence before disconnecting the stream. Immediate // disconnect (NULL) while samples are still in the DMA buffer // causes an occasional pop even when volume is zero. if (this.park_timeout_id == 0) { this.park_timeout_id = GLib.Timeout.add (LATENCY, () => { this.park_timeout_id = 0; this.park_audio_sink (); return GLib.Source.REMOVE; }); } } public bool is_playing () { return this._is_playing; } public void fade_in (int64 duration) { var running_time = this.get_running_time (); var current_fade_value = this.get_fade_value (running_time); this.unschedule_stop (); this.fade_to = 1.0; this.ensure_volume_interpolation (); if (current_fade_value == 1.0) { return; } if (duration <= 0) { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, this.fade_to); } else if (running_time == 0 || running_time == Gst.CLOCK_TIME_NONE) { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, current_fade_value); this.volume_interpolation.@set (0 + (Gst.ClockTime) duration * 1000, this.fade_to); } else { var latency = LATENCY * Gst.MSECOND; var latency_fade_value = this.get_fade_value (running_time + latency); this.volume_interpolation.unset_all (); this.volume_interpolation.@set (running_time, current_fade_value); this.volume_interpolation.@set (running_time + latency, latency_fade_value); this.volume_interpolation.@set (running_time + latency + (Gst.ClockTime) duration * 1000, this.fade_to); } if (!this._is_playing) { this.play (); } } public void fade_out (int64 duration) { var running_time = this.get_running_time (); var current_fade_value = this.get_fade_value (running_time); this.ensure_volume_interpolation (); this.unschedule_stop (); this.fade_to = 0.0; if (!this._is_playing || running_time == Gst.CLOCK_TIME_NONE || current_fade_value == 0.0) { return; } if (duration <= 0) { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, this.fade_to); this.stop (); } else { var latency = LATENCY * Gst.MSECOND; var latency_fade_value = this.get_fade_value (running_time + latency); this.volume_interpolation.unset_all (); this.volume_interpolation.@set (running_time, current_fade_value); this.volume_interpolation.@set (running_time + latency, latency_fade_value); this.volume_interpolation.@set (running_time + latency + (Gst.ClockTime) duration * 1000, this.fade_to); // GStreamer doesn't have a signal for when the interpolation is completed. // Approximate it with timeout. this.stop_timeout_id = GLib.Timeout.add ( Ft.Timestamp.to_milliseconds_uint (duration) + LATENCY, this.on_stop_timeout); } } public signal void playback_started (); public virtual signal void playback_stopped () { this.unschedule_stop (); if (this.volume_interpolation != null) { this.volume_interpolation.unset_all (); this.volume_interpolation.@set (0, 0.0); } } public signal void playback_error (GLib.Error error); public override void dispose () { this.unschedule_stop (); if (this.park_timeout_id != 0) { GLib.Source.remove (this.park_timeout_id); this.park_timeout_id = 0; } if (this.pipeline != null) { this.audio_sink.set_locked_state (false); this.pipeline.set_state (Gst.State.NULL); } this.audio_sink = null; this.volume_filter = null; this.pipeline = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/sounds.vala000066400000000000000000000144041520625676500225610ustar00rootroot00000000000000/* * Copyright (c) 2016,2024,2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { private string build_absolute_path (string path) { return GLib.Path.build_filename (Config.PACKAGE_DATA_DIR, "sounds", path); } private string build_absolute_uri (string uri) { var scheme = GLib.Uri.parse_scheme (uri); if (scheme == null && uri != "") { var preset_filename = uri; var preset_path = build_absolute_path (preset_filename); try { return GLib.Filename.to_uri (preset_path); } catch (GLib.ConvertError error) { GLib.warning ("Failed to convert \"%s\" to URI: %s", preset_path, error.message); } } return uri; } private bool is_mime_type (string content_type, string[] mime_types) { if (GLib.ContentType.is_unknown (content_type)) { return false; } foreach (var mime_type in mime_types) { if (GLib.ContentType.is_mime_type (content_type, mime_type)) { return true; } } return false; } public errordomain SoundError { NOT_FOUND, NOT_INITIALIZED, NOT_SUPPORTED } /** * A sound player instance is intended for playing a single sound multiple times. */ public abstract class Sound : GLib.Object { [CCode (notify = false)] public string uri { get { return this._uri; } set { var uri = value ?? ""; if (this._uri != uri) { this._uri = uri; this.prepare (); this.notify_property ("uri"); } else if (this.error != null) { this.prepare (); // retry } } } public double volume { get; set; default = 1.0; } public GLib.Error? error { get; private set; } private string _uri = ""; protected Ft.SoundPlayer? player = null; private void prepare () { var uri = build_absolute_uri (this._uri); try { // Validate file if (uri != "") { var file = GLib.File.new_for_uri (uri); var content_type = GLib.ContentType.guess (file.get_basename (), null, null); if (!file.query_exists ()) { throw new Ft.SoundError.NOT_FOUND (_("File not found")); } if (!is_mime_type (content_type, Ft.SoundPlayer.get_supported_mime_types ())) { throw new Ft.SoundError.NOT_SUPPORTED (_("File type not supported")); } } // Initialize player var player = uri != "" ? this.player ?? new Ft.SoundPlayer () : null; if (this.player != player) { if (this.player != null) { this.destroy_player (); } this.player = player; if (this.player != null) { this.initialize_player (); } } if (this.error != null) { this.error = null; } if (player != null) { player.uri = uri; } } catch (GLib.Error error) { GLib.warning ("Error while initializing sound player for uri=%s: %s", uri, error.message); if (this.player != null) { this.destroy_player (); this.player = null; } this.error = error; } } private void on_playback_error (GLib.Error error) { GLib.warning ("Playback error for uri=%s: %s", this.player.uri, error.message); this.error = error; } public bool can_play () { return this.player != null; } public bool is_playing () { return this.player != null && this.player.is_playing (); } public void play () { if (this.player == null && this._uri != "") { this.prepare (); } if (this.player != null) { this.player.play (); } } public void stop () { this.player?.stop (); } protected virtual void initialize_player () { this.bind_property ("volume", this.player, "volume", GLib.BindingFlags.SYNC_CREATE); this.player.playback_error.connect (this.on_playback_error); } protected virtual void destroy_player () { this.player.playback_error.disconnect (this.on_playback_error); this.player.stop (); } public virtual void destroy () { if (this.player != null) { this.destroy_player (); this.player = null; } } } public class AlertSound : Ft.Sound { public string event_id { get; construct; } public AlertSound (string event_id) { GLib.Object ( event_id: event_id ); } } public class BackgroundSound : Ft.Sound { public bool loop { get; set; default = false; } protected override void initialize_player () { base.initialize_player (); this.bind_property ("loop", this.player, "loop", GLib.BindingFlags.SYNC_CREATE); } public void fade_in (int64 duration) { this.player?.fade_in (duration); } public void fade_out (int64 duration) { this.player?.fade_out (duration); } } } focustimerhq-FocusTimer-8581be2/src/core/state.vala000066400000000000000000000063571520625676500223760ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { /** * Ft.State * * In general, there are main states STOPPED, POMODORO and BREAK. * BREAK may be resolved to either SHORT_BREAK or LONG_BREAK by the session-manager / scheduler, * but can function on its own if session has no cycles. */ public enum State { STOPPED, POMODORO, BREAK, SHORT_BREAK, LONG_BREAK; public string to_string () { switch (this) { case STOPPED: return "stopped"; case POMODORO: return "pomodoro"; case BREAK: return "break"; case SHORT_BREAK: return "short-break"; case LONG_BREAK: return "long-break"; default: assert_not_reached (); } } public static Ft.State from_string (string? state) { switch (state) { case "pomodoro": return POMODORO; case "break": return BREAK; case "short-break": return SHORT_BREAK; case "long-break": return LONG_BREAK; default: return STOPPED; } } public string get_label () { switch (this) { case STOPPED: return _("Stopped"); case POMODORO: return _("Pomodoro"); case BREAK: return _("Break"); case SHORT_BREAK: return _("Short Break"); case LONG_BREAK: return _("Long Break"); default: assert_not_reached (); } } public bool is_a (Ft.State other) { if (this == BREAK) { return other.is_break (); } if (other == Ft.State.BREAK) { return this.is_break (); } return this == other; } public bool is_break () { return this == BREAK || this == SHORT_BREAK || this == LONG_BREAK; } public int64 get_default_duration () { var settings = Ft.get_settings (); uint seconds; switch (this) { case POMODORO: seconds = settings.get_uint ("pomodoro-duration"); break; case BREAK: case SHORT_BREAK: seconds = settings.get_uint ("short-break-duration"); break; case LONG_BREAK: seconds = settings.get_uint ("long-break-duration"); break; default: seconds = 0; break; } return (int64) seconds * Ft.Interval.SECOND; } } } focustimerhq-FocusTimer-8581be2/src/core/stats-entry.vala000066400000000000000000000034761520625676500235520ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public class StatsEntry : Gom.Resource { public int64 id { get; set; } public int64 time { get; set; } public string date { get; set; } public int64 offset { get; set; } public int64 duration { get; set; } public string category { get; set; } public int64 source_id { get; set; default = 0; } static construct { set_table ("stats"); set_primary_key ("id"); set_notnull ("time"); set_notnull ("date"); set_notnull ("offset"); set_notnull ("category"); // `source-id` may reference `timeblocks` and `gaps` tables. // Therefore, we treat it like an integer. } public Ft.StatsEntry copy () { return (Ft.StatsEntry) GLib.Object.@new ( typeof (Ft.StatsEntry), id: this.id, time: this.time, date: this.date, offset: this.offset, duration: this.duration, category: this.category, source_id: this.source_id); } } /** * Model for aggregated daily stats */ public class AggregatedStatsEntry : Gom.Resource { public int64 id { get; set; } public string date { get; set; } public string category { get; set; } public int64 duration { get; set; } public int64 count { get; set; } static construct { set_table ("aggregatedstats"); set_primary_key ("id"); set_notnull ("date"); set_notnull ("category"); } } } focustimerhq-FocusTimer-8581be2/src/core/stats-manager.vala000066400000000000000000000547111520625676500240210ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Class responsible for time-tracking and collecting stats */ [SingleInstance] public sealed class StatsManager : GLib.Object { public const int64 MIDNIGHT_OFFSET = 4 * Ft.Interval.HOUR; // 4 AM private struct Segment { public int64 timestamp; public int64 duration; public GLib.DateTime datetime; public inline bool is_valid () { return Ft.Timestamp.is_defined (this.timestamp) && this.datetime != null && this.duration > 0; } } private struct Item { public string category; public int64 source_id; public Segment[] segments; } [Compact] private class Callback { public GLib.SourceFunc func; public Callback (owned GLib.SourceFunc func) { this.func = (owned) func; } } public unowned Ft.SessionManager session_manager { get { return this._session_manager; } construct { this._session_manager = value; this._session_manager.time_block_saved.connect (this.on_time_block_saved); this._session_manager.gap_saved.connect (this.on_gap_saved); } } private Ft.SessionManager? _session_manager = null; private Ft.TimezoneHistory? timezone_history = null; private Ft.AsyncQueue queue = null; private bool is_processing = false; private Callback[] flush_callbacks; construct { this.timezone_history = new Ft.TimezoneHistory (); this.queue = new Ft.AsyncQueue (); this.flush_callbacks = {}; } public StatsManager () { GLib.Object ( session_manager: Ft.SessionManager.get_default () ); } private GLib.DateTime? transform_timestamp (int64 timestamp) { var timezone = this.timezone_history.search (timestamp); if (timezone == null) { GLib.warning ("Did not find timezone for timestamp %s", timestamp.to_string ()); } return Ft.Timestamp.to_datetime (timestamp, timezone); } private void transform_datetime (GLib.DateTime datetime, out GLib.Date date, out int64 offset) { date = GLib.Date (); date.set_dmy ((GLib.DateDay) datetime.get_day_of_month (), (GLib.DateMonth) datetime.get_month (), (GLib.DateYear) datetime.get_year ()); offset = datetime.get_hour () * Ft.Interval.HOUR + datetime.get_minute () * Ft.Interval.MINUTE + datetime.get_second () * Ft.Interval.SECOND + datetime.get_microsecond (); // Adjust for virtual midnight if (offset < MIDNIGHT_OFFSET) { date.subtract_days (1U); offset += 24 * Ft.Interval.HOUR; } } private string? transform_state (Ft.State state) { switch (state) { case Ft.State.POMODORO: return "pomodoro"; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: return "break"; default: return null; } } /** * Find timestamps where an entry should be divided: midnights, time-zone changes, etc. * `timestamp` is returned as the first split. */ private Segment[] split (int64 timestamp, int64 duration) { Segment[] segments = {}; if (Ft.Timestamp.is_undefined (timestamp) || duration <= 0) { return segments; } this.timezone_history.scan ( timestamp, timestamp + duration, (start_time, end_time, timezone) => { var datetime = this.transform_timestamp (start_time); if (datetime == null) { GLib.warning ("Failed to convert timestamp %s", start_time.to_string ()); return; } segments += Segment () { timestamp = start_time, duration = 0, datetime = datetime }; var midnight = new GLib.DateTime ( timezone, datetime.get_year (), datetime.get_month (), datetime.get_day_of_month (), 0, 0, 0); midnight = midnight.add_seconds ( Ft.Timestamp.to_seconds (MIDNIGHT_OFFSET)); while (true) { var midnight_timestamp = Ft.Timestamp.from_datetime (midnight); if (midnight_timestamp > end_time) { break; } if (midnight_timestamp < start_time) { midnight = midnight.add_days (1); continue; } segments += Segment () { timestamp = midnight_timestamp, duration = 0, datetime = midnight }; midnight = midnight.add_days (1); } }); for (var index = 0; index < segments.length - 1; index++) { segments[index].duration = segments[index + 1].timestamp - segments[index].timestamp; } if (segments.length > 0) { segments[segments.length - 1].duration = timestamp + duration - segments[segments.length - 1].timestamp; } return segments; } /** * Convenience method to ensure entries are saved in database. * * It's intended for testing `StatsManager` alone. If a `SessionManager` is used, * the session manager is responsible for saving time-block/gap entries. */ private void try_save_time_block (Ft.TimeBlock time_block) { if (!Ft.is_test () || time_block.session == null) { return; } if (this._session_manager.current_session != null) { return; } try { var session_entry = time_block.session.create_or_update_entry (); session_entry?.save_sync (); var time_block_entry = time_block.create_or_update_entry (); time_block_entry?.save_sync (); } catch (GLib.Error error) { GLib.critical ("Error saving time-block: %s", error.message); } time_block.foreach_gap ( (gap) => { var gap_entry = gap.create_or_update_entry (); if (gap_entry != null) { try { gap_entry.save_sync (); } catch (GLib.Error error) { GLib.critical ("Error saving gap: %s", error.message); } } }); } private async Gom.ResourceGroup fetch_entries (Gom.Repository repository, string category, int64 timestamp, int64 source_id) throws GLib.Error { Gom.Filter filter; var category_value = GLib.Value (typeof (string)); category_value.set_string (category); var category_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "category", category_value); if (source_id != 0) { var source_id_value = GLib.Value (typeof (int64)); source_id_value.set_int64 (source_id); var source_id_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "source-id", source_id_value); filter = new Gom.Filter.and (category_filter, source_id_filter); } else { var time_value = GLib.Value (typeof (int64)); time_value.set_int64 (timestamp); var time_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "time", time_value); filter = new Gom.Filter.and (category_filter, time_filter); } return yield repository.find_async (typeof (Ft.StatsEntry), filter); } private async void process_item (Item item) throws GLib.Error { var category = item.category; var source_id = item.source_id; var segments = item.segments; var repository = Ft.Database.get_repository (); var entries = yield this.fetch_entries (repository, category, segments[0].timestamp, source_id); var entry_index = 0; var to_save = (Gom.ResourceGroup) GLib.Object.@new ( typeof (Gom.ResourceGroup), repository: repository, resource_type: typeof (Ft.StatsEntry), is_writable: true); var to_save_count = 0; var to_delete = (Gom.ResourceGroup) GLib.Object.@new ( typeof (Gom.ResourceGroup), repository: repository, resource_type: typeof (Ft.StatsEntry), is_writable: true); var to_delete_count = 0; Ft.StatsEntry[] saved_entries = {}; Ft.StatsEntry[] deleted_entries = {}; if (entries.count > 0) { yield entries.fetch_async (0U, entries.count); } foreach (var segment in segments) { Ft.StatsEntry entry; GLib.Date date; int64 offset; if (!segment.is_valid ()) { continue; } if (entry_index < entries.count) { entry = (Ft.StatsEntry?) entries.get_index (entry_index); entry_index++; if (entry.time == segment.timestamp && entry.duration == segment.duration) { continue; } deleted_entries += entry.copy (); // TODO: test if editing entries work OK } else { entry = new Ft.StatsEntry (); entry.repository = repository; } this.transform_datetime (segment.datetime, out date, out offset); entry.category = category; entry.time = segment.timestamp; entry.date = Ft.Database.serialize_date (date); entry.offset = offset; entry.duration = segment.duration; entry.source_id = source_id; to_save.append (entry); to_save_count++; saved_entries += entry; } to_save.write_sync (); // yield to_save.write_async (); // XXX: should use async while (entry_index < entries.count) { var entry = (Ft.StatsEntry?) entries.get_index (entry_index); entry_index++; assert (entry != null); deleted_entries += entry; to_delete.append (entry); to_delete_count++; } if (to_delete != null) { yield to_delete.delete_async (); } foreach (var entry in saved_entries) { this.entry_saved (entry); } foreach (var entry in deleted_entries) { this.entry_deleted (entry); } } private async void process_queue () { if (this.is_processing) { return; } this.is_processing = true; Item? item; while ((item = this.queue.pop ()) != null) { try { yield this.process_item (item); } catch (GLib.Error error) { GLib.warning ("Error while processing stats item: %s", error.message); } } this.is_processing = false; // Resume any waiting `flush()` calls foreach (unowned var callback in this.flush_callbacks) { callback.func (); } this.flush_callbacks = {}; } private void track_internal (string category, int64 source_id, owned Segment[] segments) { if (segments.length == 0) { return; } this.queue.push ( Item () { category = category, source_id = source_id, segments = segments }); this.process_queue.begin (); } public void track (string category, int64 timestamp, int64 duration, int64 source_id = 0) { if (Ft.Timestamp.is_undefined (timestamp) || duration <= 0) { return; } var segments = this.split (timestamp, duration); this.track_internal (category, source_id, segments); } /** * Finds the intersection of two arrays of segments. * Returns a new array containing segments that represent overlapping time periods. * Assume that both arrays are sorted. */ private Segment[] intersect_segments (owned Segment[] a, owned Segment[] b) { Segment[] result = {}; var b_index = 0; var b_length = b.length; // For each segment in array `a`, find intersections with array `b` foreach (var segment_a in a) { var a_start = segment_a.timestamp; var a_end = a_start + segment_a.duration; // Skip segments in `b` that end before the current segment in `a` starts while (b_index < b_length && b[b_index].timestamp + b[b_index].duration <= a_start) { b_index++; } // If we've gone through all segments in `b`, we're done if (b_index >= b_length) { break; } // Check for intersections with segments in `b` for (var tmp_index = b_index; tmp_index < b_length; tmp_index++) { var segment_b = b[tmp_index]; var b_start = segment_b.timestamp; var b_end = b_start + segment_b.duration; if (b_start >= a_end) { break; } if (a_start < b_end && a_end > b_start) { var intersection_start = int64.max (a_start, b_start); var intersection_end = int64.min (a_end, b_end); result += Segment () { timestamp = intersection_start, duration = intersection_end - intersection_start, datetime = intersection_start == a_start ? segment_a.datetime : segment_b.datetime }; } } } return result; } public void track_time_block (Ft.TimeBlock time_block) { this.try_save_time_block (time_block); unowned var time_block_entry = time_block.entry; var timestamp = time_block.start_time; var category = this.transform_state (time_block.state); Segment[] segments = {}; if (time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED) { // Assume that we do not change status back to SCHEDULED. // Otherwise, we should delete stats entries. return; } if (category == null) { GLib.warning ("Skip tracking %s: Missing category.", time_block.state.to_string ()); return; } if (time_block_entry == null) { GLib.warning ("Skip tracking %s: Missing source entry.", time_block.state.to_string ()); return; } time_block.foreach_gap ( (gap) => { if (gap.start_time > timestamp) { segments += Segment () { timestamp = timestamp, duration = gap.start_time - timestamp, datetime = this.transform_timestamp (timestamp) }; } timestamp = gap.end_time; }); if (Ft.Timestamp.is_defined (time_block.end_time) && time_block.end_time > timestamp && time_block.get_status () != Ft.TimeBlockStatus.IN_PROGRESS) { segments += Segment () { timestamp = timestamp, duration = time_block.end_time - timestamp, datetime = this.transform_timestamp (timestamp) }; } segments = this.intersect_segments ( segments, this.split (time_block.start_time, time_block.duration)); this.track_internal (category, time_block_entry.id, segments); } public void track_gap (Ft.Gap gap) { this.try_save_time_block (gap.time_block); unowned var gap_entry = gap.entry; if (gap_entry == null) { GLib.warning ("Skipping tracking gap. Missing source entry."); return; } if (Ft.Timestamp.is_undefined (gap.end_time) || gap.time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED) { return; } if (gap.has_flag (Ft.GapFlags.INTERRUPTION)) { this.track ("interruption", gap.start_time, gap.duration, gap_entry.id); } } public async void flush () { yield this.queue.wait (); if (this.is_processing) { this.flush_callbacks += new Callback (this.flush.callback); yield; } } public GLib.DateTime get_midnight (GLib.Date date) { var timezone = this.timezone_history.search_by_date ( date, Ft.StatsManager.MIDNIGHT_OFFSET); if (timezone == null) { timezone = new GLib.TimeZone.local (); } var midnight_hour = (int) ( Ft.StatsManager.MIDNIGHT_OFFSET / Ft.Interval.HOUR); var midnight = new GLib.DateTime ( timezone, date.get_year (), date.get_month (), date.get_day (), midnight_hour, 0, 0); return midnight; } /** * Return todays date adjusted for virtual midnight. */ public GLib.Date get_today () { var datetime = this.transform_timestamp (Ft.Timestamp.from_now ()); var today = GLib.Date (); this.transform_datetime (datetime, out today, null); return today; } private void on_time_block_saved (Ft.TimeBlock time_block, Ft.TimeBlockEntry time_block_entry) { this.track_time_block (time_block); } private void on_gap_saved (Ft.Gap gap, Ft.GapEntry gap_entry) { this.track_gap (gap); } public signal void entry_saved (Ft.StatsEntry entry); public signal void entry_deleted (Ft.StatsEntry entry); public override void dispose () { if (this._session_manager != null) { this._session_manager.time_block_saved.disconnect (this.on_time_block_saved); this._session_manager.gap_saved.disconnect (this.on_gap_saved); this._session_manager = null; } this.timezone_history = null; this.queue = null; this.flush_callbacks = {}; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/time-block-entry.vala000066400000000000000000000017341520625676500244350ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public class TimeBlockEntry : Gom.Resource { public int64 id { get; set; } public int64 session_id { get; set; } public int64 start_time { get; set; } public int64 end_time { get; set; } public string state { get; set; } public string status { get; set; } public int64 intended_duration { get; set; } internal ulong version = 0; static construct { set_table ("timeblocks"); set_primary_key ("id"); set_notnull ("session-id"); set_notnull ("start-time"); set_notnull ("end-time"); set_notnull ("state"); set_notnull ("status"); set_notnull ("intended-duration"); set_unique ("start-time"); set_reference ("session-id", "sessions", "id"); } } } focustimerhq-FocusTimer-8581be2/src/core/time-block.vala000066400000000000000000000573671520625676500233130ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Ft.TimeBlockStatus enum. * * A time-block status is managed at a session-level by `SessionManager`. */ public enum TimeBlockStatus { SCHEDULED = 0, IN_PROGRESS = 1, COMPLETED = 2, UNCOMPLETED = 3; public string to_string () { switch (this) { case SCHEDULED: return "scheduled"; case IN_PROGRESS: return "in-progress"; case COMPLETED: return "completed"; case UNCOMPLETED: return "uncompleted"; default: assert_not_reached (); } } public static Ft.TimeBlockStatus from_string (string? status) { switch (status) { case "in-progress": return IN_PROGRESS; case "completed": return COMPLETED; case "uncompleted": return UNCOMPLETED; default: return SCHEDULED; } } } /** * Ft.TimeBlockMeta struct. * * Some properties of the `TimeBlock` are purely external and should not trigger * `TimeBlock.changed` signal. `TimeBlockMeta` is a convenience structure for read-only. */ public struct TimeBlockMeta { public Ft.TimeBlockStatus status; public int64 intended_duration; public double weight; public int64 completion_time; } public interface Schedulable : GLib.Object { public abstract int64 start_time { get; set; } public abstract int64 end_time { get; set; } public abstract int64 duration { get; set; } public bool has_started (int64 timestamp = Ft.Timestamp.UNDEFINED) { var start_time = this.start_time; if (Ft.Timestamp.is_undefined (start_time)) { return true; } Ft.ensure_timestamp (ref timestamp); return timestamp >= start_time; } public bool has_ended (int64 timestamp = Ft.Timestamp.UNDEFINED) { var end_time = this.end_time; if (Ft.Timestamp.is_undefined (end_time)) { return false; } Ft.ensure_timestamp (ref timestamp); return timestamp > end_time; } public static int compare (Ft.Schedulable a, Ft.Schedulable b) { return (int) (a.start_time > b.start_time) - (int) (a.start_time < b.start_time); } public abstract void set_time_range (int64 start_time, int64 end_time); public abstract void move_by (int64 offset); public abstract void move_to (int64 start_time); } public class TimeBlock : GLib.InitiallyUnowned, Ft.Schedulable { public Ft.State state { get { return this._state; } construct { this._state = value; } } public weak Ft.Session session { get; set; } [CCode (notify = false)] public int64 start_time { get { return this._start_time; } set { if (this._start_time == value) { return; } if (Ft.Timestamp.is_undefined (value) || Ft.Timestamp.is_undefined (this._end_time) || value <= this._end_time) { this.set_time_range (value, this._end_time); } else { this.set_time_range (value, value); } } } [CCode (notify = false)] public int64 end_time { get { return this._end_time; } set { if (this._end_time == value) { return; } if (Ft.Timestamp.is_undefined (value) || Ft.Timestamp.is_undefined (this._start_time) || value >= this._start_time) { this.set_time_range (this._start_time, value); } else { this.set_time_range (value, value); } } } /** * `duration` of a time block, including gaps */ [CCode (notify = false)] public int64 duration { get { return Ft.Timestamp.subtract (this._end_time, this._start_time); } set { if (Ft.Timestamp.is_defined (this._start_time)) { this.set_time_range (this._start_time, Ft.Timestamp.add_interval (this._start_time, value)); } else { GLib.warning ("Can't change time-block duration without a defined start-time."); } } } internal ulong version = 0; internal Ft.TimeBlockEntry? entry = null; private GLib.List gaps = null; private int64 _start_time = Ft.Timestamp.UNDEFINED; private int64 _end_time = Ft.Timestamp.UNDEFINED; private Ft.State _state = Ft.State.STOPPED; private int changed_freeze_count = 0; private bool changed_is_pending = false; private Ft.TimeBlockMeta meta; construct { this.meta = Ft.TimeBlockMeta() { status = Ft.TimeBlockStatus.SCHEDULED, intended_duration = 0, weight = double.NAN, completion_time = Ft.Timestamp.UNDEFINED }; } public TimeBlock (Ft.State state = Ft.State.STOPPED) { GLib.Object ( state: state ); } public TimeBlock.with_start_time (int64 start_time, Ft.State state = Ft.State.STOPPED) { GLib.Object ( state: state ); this.set_time_range ( start_time, Ft.Timestamp.add_interval (start_time, state.get_default_duration ()) ); } private void emit_changed () { this.version++; if (this.changed_freeze_count > 0) { this.changed_is_pending = true; } else { this.changed_is_pending = false; this.changed (); } } /** * Increases the freeze count on this. */ public void freeze_changed () { this.changed_freeze_count++; } /** * Decrease the freeze count on this. */ public void thaw_changed () { this.changed_freeze_count--; if (this.changed_freeze_count == 0) { this.emit_changed (); } } public void set_time_range (int64 start_time, int64 end_time) { var old_start_time = this._start_time; var old_end_time = this._end_time; var old_duration = this._end_time - this._start_time; var changed = false; this._start_time = start_time; this._end_time = end_time; if (this._start_time != old_start_time) { this.notify_property ("start-time"); changed = true; } if (this._end_time != old_end_time) { this.notify_property ("end-time"); changed = true; } if (this._end_time - this._start_time != old_duration) { this.notify_property ("duration"); } if (changed) { this.emit_changed (); } } public void move_by (int64 offset) { if (offset == 0) { return; } var start_time = Ft.Timestamp.is_defined (this._start_time) ? Ft.Timestamp.add_interval (this._start_time, offset) : Ft.Timestamp.UNDEFINED; var end_time = Ft.Timestamp.is_defined (this._end_time) ? Ft.Timestamp.add_interval (this._end_time, offset) : Ft.Timestamp.UNDEFINED; this.freeze_changed (); this.gaps.@foreach ((gap) => gap.move_by (offset)); this.set_time_range (start_time, end_time); this.thaw_changed (); } public void move_to (int64 start_time) { if (Ft.Timestamp.is_undefined (this._start_time) && Ft.Timestamp.is_undefined (this._end_time)) { if (!this.gaps.is_empty ()) { GLib.warning ("Unable to move time-block gaps. Time-block start-time is undefined."); } this.set_time_range (start_time, this._end_time); return; } if (Ft.Timestamp.is_undefined (this._start_time)) { GLib.warning ("Unable to move time-block. Time-block start-time is undefined."); return; } this.move_by (Ft.Timestamp.subtract (start_time, this._start_time)); } /** * Calculate elapsed time excluding gaps/interruptions. */ public int64 calculate_elapsed (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); if (Ft.Timestamp.is_undefined (this._start_time)) { return 0; // Result won't make sense if block has no `start`. } if (this._start_time >= timestamp || this._start_time >= this._end_time) { return 0; } var range_start = this._start_time; var range_end = Ft.Timestamp.is_defined (this._end_time) ? int64.min (this._end_time, timestamp) : timestamp; var elapsed = Ft.Timestamp.subtract (range_end, range_start); this.gaps.@foreach ( (gap) => { var gap_start_time = gap.start_time.clamp (range_start, range_end); var gap_end_time = gap.end_time; if (Ft.Timestamp.is_undefined (gap_end_time)) { gap_end_time = range_end; } else if (gap_end_time > gap_start_time) { gap_end_time = gap_end_time.clamp (range_start, range_end); } else { return; } elapsed = Ft.Interval.subtract ( elapsed, Ft.Timestamp.subtract (gap_end_time, gap_start_time)); range_start = gap_end_time; }); return elapsed; } /** * Calculate remaining time excluding gaps/interruptions. */ public int64 calculate_remaining (int64 timestamp = Ft.Timestamp.UNDEFINED) { Ft.ensure_timestamp (ref timestamp); if (Ft.Timestamp.is_undefined (this._end_time)) { return 0; // Result won't make sense if block has no `end`. } var last_gap = this.get_last_gap (); if (last_gap != null && Ft.Timestamp.is_undefined (last_gap.end_time)) { timestamp = int64.min (last_gap.start_time, timestamp); } var range_start = int64.max (this._start_time, timestamp); var range_end = this._end_time; var remaining = Ft.Timestamp.subtract (range_end, range_start); this.gaps.@foreach ( (gap) => { var gap_start_time = gap.start_time.clamp (range_start, range_end); var gap_end_time = gap.end_time; if (Ft.Timestamp.is_undefined (gap_end_time) || gap_start_time >= gap_end_time) { return; } gap_end_time = gap_end_time.clamp (range_start, range_end); remaining = Ft.Interval.subtract ( remaining, Ft.Timestamp.subtract (gap_end_time, gap_start_time)); range_start = gap_end_time.clamp (range_start, range_end); }); return int64.max (remaining, 0); } /** * Calculate progress - elapsed time compared to completion-time. */ public double calculate_progress (int64 timestamp) { Ft.ensure_timestamp (ref timestamp); if (this.meta.status == Ft.TimeBlockStatus.SCHEDULED || this.meta.status == Ft.TimeBlockStatus.UNCOMPLETED) { return 0.0; } if (this.meta.status == Ft.TimeBlockStatus.COMPLETED) { return 1.0; } if (Ft.Timestamp.is_undefined (this._start_time) || Ft.Timestamp.is_undefined (this._end_time)) { return 0.0; // Result won't make sense if block has no `start`. } if (this._start_time >= timestamp || this._start_time >= this._end_time) { return 0.0; } var range_start = this._start_time; var range_end = Ft.Timestamp.is_defined (this.meta.completion_time) ? this.meta.completion_time : this._end_time; var duration = Ft.Timestamp.subtract (range_end, range_start); var elapsed = Ft.Timestamp.subtract (timestamp, range_start); this.gaps.@foreach ( (gap) => { if (Ft.Timestamp.is_undefined (gap.end_time)) { elapsed = Ft.Interval.subtract ( elapsed, Ft.Timestamp.subtract ( timestamp, gap.start_time.clamp (range_start, timestamp) ) ); range_start = range_end; return; } if (gap.end_time <= gap.start_time) { return; } duration = Ft.Interval.subtract ( duration, Ft.Timestamp.subtract ( gap.end_time.clamp (range_start, range_end), gap.start_time.clamp (range_start, range_end) ) ); elapsed = Ft.Interval.subtract ( elapsed, Ft.Timestamp.subtract ( gap.end_time.clamp (range_start, timestamp), gap.start_time.clamp (range_start, timestamp) ) ); range_start = gap.end_time.clamp (range_start, range_end); } ); return duration > 0 ? (double) elapsed / (double) duration : 0.0; } private void on_gap_changed (Ft.Gap gap) { this.emit_changed (); } public void add_gap (Ft.Gap gap) { if (gap.time_block == this) { return; } if (gap.time_block != null) { gap.time_block.remove_gap (gap); } gap.time_block = this; gap.changed.connect (this.on_gap_changed); this.gaps.insert_sorted (gap, Ft.Schedulable.compare); this.emit_changed (); } public void remove_gap (Ft.Gap gap) { if (gap.time_block != this) { return; } gap.changed.disconnect (this.on_gap_changed); gap.time_block = null; this.gaps.remove (gap); this.emit_changed (); } public unowned Ft.Gap? get_last_gap () { unowned GLib.List link = this.gaps.last (); return link != null ? link.data : null; } public unowned Ft.Gap? get_nth_gap (uint index) { return this.gaps.nth_data (index); } public void foreach_gap (GLib.Func func) { this.gaps.@foreach (func); } private void remove_link (GLib.List? link) { if (link == null) { return; } link.data = null; this.gaps.delete_link (link); } /** * Cleanup gaps. * * Handling of overlapped gaps is tailored for the rewind action. */ public void normalize_gaps () { unowned GLib.List link = this.gaps.last (); unowned GLib.List tmp; var changed = false; this.freeze_changed (); // assume that gaps are sorted while (link != null) { // Remove invalid gaps. if (Ft.Timestamp.is_defined (link.data.end_time) && link.data.end_time < link.data.start_time || Ft.Timestamp.is_undefined (link.data.start_time)) { GLib.debug ("normalize_gaps: removing invalid gap"); tmp = link.prev; this.remove_link (link); link = tmp; changed = true; continue; } // Handle overlapping gaps. if (link.next != null && link.data.end_time > link.next.data.start_time) { var overlap = link.data.end_time - link.next.data.start_time; if (Ft.Timestamp.is_undefined (link.next.data.end_time)) { link.data.move_by (-overlap); link = link.prev; changed = overlap > 0 ? true : changed; } else { tmp = link.prev; link.next.data.start_time = Ft.Timestamp.subtract_interval (link.data.start_time, overlap); this.remove_link (link); link = tmp; changed = true; } continue; } link = link.prev; } if (changed) { this.emit_changed (); } this.thaw_changed (); } /** * We don't allow changing of `TimeBlock.state` after the time-block changes status to in-progress. * However, it's allowed to change state of a scheduled time-block. */ internal void set_state_internal (Ft.State state) { if (this._state == state) { return; } this._state = state; this.notify_property ("state"); this.emit_changed (); } /* * Metadata */ public Ft.TimeBlockMeta get_meta () { return this.meta; } public void set_meta (Ft.TimeBlockMeta meta) { this.meta = meta; this.version++; // this.emit_changed (); // TODO } /** * Convenience alias for `Session.get_time_block_status(...)` */ public Ft.TimeBlockStatus get_status () { return this.meta.status; } /** * Convenience alias for `Session.set_time_block_status(...)` */ public void set_status (Ft.TimeBlockStatus status) { if (this.meta.status != status) { this.meta.status = status; this.version++; // this.emit_changed (); // TODO } } public int64 get_intended_duration () { return this.meta.intended_duration; } public void set_intended_duration (int64 intended_duration) { if (this.meta.intended_duration != intended_duration) { this.meta.intended_duration = intended_duration; this.emit_changed (); } } public double get_weight () { return this.meta.weight; } public void set_weight (double weight) { if (this.meta.weight != weight) { this.meta.weight = weight; this.emit_changed (); } } public int64 get_completion_time () { return this.meta.completion_time; } public void set_completion_time (int64 completion_time) { if (this.meta.completion_time != completion_time) { this.meta.completion_time = completion_time; this.emit_changed (); } } /* * Database */ internal bool should_create_entry () { // XXX: we may want to save SCHEDULED time-blocks in future, e.g. when setting up // a custom session / warm-up. return this.meta.status != Ft.TimeBlockStatus.SCHEDULED && this.state != Ft.State.STOPPED && Ft.Timestamp.is_defined (this.start_time); } internal bool should_update_entry () { if (this.entry == null || this.entry.id == 0) { return true; } return this.entry.version != this.version || this.entry.status != this.meta.status.to_string (); } internal unowned Ft.TimeBlockEntry create_or_update_entry () requires (this.session != null) { if (this.entry == null) { this.entry = new Ft.TimeBlockEntry (); this.entry.repository = Ft.Database.get_repository (); this.session.entry.bind_property ("id", this.entry, "session-id", GLib.BindingFlags.SYNC_CREATE); } this.entry.start_time = this._start_time; this.entry.end_time = this._end_time; this.entry.state = this._state.to_string (); this.entry.status = this.meta.status.to_string (); this.entry.intended_duration = this.meta.intended_duration; this.entry.version = this.version; return this.entry; } internal void unset_entry () { unowned GLib.List link = this.gaps.first (); while (link != null) { link.data.unset_entry (); link = link.next; } this.entry = null; } /* * Signals */ public signal void changed (); public override void dispose () { this.entry = null; this.gaps = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/timer-action-group.vala000066400000000000000000000142131520625676500247710ustar00rootroot00000000000000/* * Copyright (c) 2016-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public class TimerActionGroup : GLib.SimpleActionGroup { public Ft.Timer timer { get; construct; } public TimerActionGroup () { GLib.Object ( timer: Ft.Timer.get_default () ); } public TimerActionGroup.with_timer (Ft.Timer timer) { GLib.Object ( timer: timer ); } construct { var start_action = new GLib.SimpleAction ("start", null); start_action.activate.connect (this.activate_start); this.add_action (start_action); var reset_action = new GLib.SimpleAction ("reset", null); reset_action.activate.connect (this.activate_reset); this.add_action (reset_action); var pause_action = new GLib.SimpleAction ("pause", null); pause_action.activate.connect (this.activate_pause); this.add_action (pause_action); var resume_action = new GLib.SimpleAction ("resume", null); resume_action.activate.connect (this.activate_resume); this.add_action (resume_action); var rewind_action = new GLib.SimpleAction ("rewind", null); rewind_action.activate.connect (this.activate_rewind); this.add_action (rewind_action); var rewind_by_action = new GLib.SimpleAction ("rewind-by", GLib.VariantType.INT32); rewind_by_action.activate.connect (this.activate_rewind); this.add_action (rewind_by_action); var toggle_action = new GLib.SimpleAction ("toggle", null); // alias for start-stop toggle_action.activate.connect (this.activate_start_stop); this.add_action (toggle_action); var start_stop_action = new GLib.SimpleAction ("start-stop", null); start_stop_action.activate.connect (this.activate_start_stop); this.add_action (start_stop_action); var start_pause_resume_action = new GLib.SimpleAction ("start-pause-resume", null); start_pause_resume_action.activate.connect (this.activate_start_pause_resume); this.add_action (start_pause_resume_action); var shorten_action = new GLib.SimpleAction ("shorten", null); shorten_action.activate.connect (this.activate_shorten); this.add_action (shorten_action); var extend_action = new GLib.SimpleAction ("extend", null); extend_action.activate.connect (this.activate_extend); this.add_action (extend_action); var extend_by_action = new GLib.SimpleAction ("extend-by", GLib.VariantType.INT32); extend_by_action.activate.connect (this.activate_extend); this.add_action (extend_by_action); } private void activate_start (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("timer.start"); this.timer.start (); } private void activate_reset (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("timer.reset"); this.timer.reset (); } private void activate_pause (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("timer.pause"); this.timer.pause (); } private void activate_resume (GLib.SimpleAction action, GLib.Variant? parameter) { Ft.Context.set_event_source ("timer.resume"); this.timer.resume (); } private void activate_rewind (GLib.SimpleAction action, GLib.Variant? parameter) { var interval = parameter != null ? parameter.get_int32 () * Ft.Interval.SECOND : Ft.Interval.MINUTE; Ft.Context.set_event_source ("timer.rewind"); this.timer.rewind (interval); } private void activate_start_stop (GLib.SimpleAction action, GLib.Variant? parameter) { if (!this.timer.is_started ()) { Ft.Context.set_event_source ("timer.start"); this.timer.start (); } else { Ft.Context.set_event_source ("timer.reset"); this.timer.reset (); } } private void activate_start_pause_resume (GLib.SimpleAction action, GLib.Variant? parameter) { if (!this.timer.is_started ()) { Ft.Context.set_event_source ("timer.start"); this.timer.start (); } else if (this.timer.is_paused ()) { Ft.Context.set_event_source ("timer.resume"); this.timer.resume (); } else { Ft.Context.set_event_source ("timer.pause"); this.timer.pause (); } } private void activate_shorten (GLib.SimpleAction action, GLib.Variant? parameter) { var interval = parameter != null ? parameter.get_int32 () * Ft.Interval.SECOND : Ft.Interval.MINUTE; Ft.Context.set_event_source ("timer.extend"); this.timer.extend (-interval); } private void activate_extend (GLib.SimpleAction action, GLib.Variant? parameter) { var interval = parameter != null ? parameter.get_int32 () * Ft.Interval.SECOND : Ft.Interval.MINUTE; Ft.Context.set_event_source ("timer.extend"); this.timer.extend (interval); } } } focustimerhq-FocusTimer-8581be2/src/core/timer.vala000066400000000000000000001115101520625676500223620ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Helper structure for changing several fields at once. Together they can be regarded as a timer state. * * `user_data` in our use refers to a time block, but don't over use it. */ [Immutable] public struct TimerState { public int64 duration; public int64 offset; public int64 started_time; public int64 paused_time; public int64 finished_time; public void* user_data; public TimerState () { this.duration = 0; this.offset = 0; this.started_time = Ft.Timestamp.UNDEFINED; this.paused_time = Ft.Timestamp.UNDEFINED; this.finished_time = Ft.Timestamp.UNDEFINED; this.user_data = null; } /** * Make state copy * * This function is unnecessary. Structs in vala are copied by default. It's kept * to bring more clarity to our code. */ public Ft.TimerState copy () { return this; } public bool equals (Ft.TimerState other) { return this.duration == other.duration && this.offset == other.offset && this.started_time == other.started_time && this.paused_time == other.paused_time && this.finished_time == other.finished_time && this.user_data == other.user_data; } public bool is_valid () { // Negative duration. if (this.duration < 0) { return false; } // Finished, but not started. if (Ft.Timestamp.is_defined (this.finished_time) && Ft.Timestamp.is_undefined (this.started_time)) { return false; } // Finished and still paused. if (Ft.Timestamp.is_defined (this.finished_time) && Ft.Timestamp.is_defined (this.paused_time)) { return false; } // Paused before started. if (Ft.Timestamp.is_defined (this.paused_time) && this.paused_time < this.started_time) { return false; } // Finished before started. if (Ft.Timestamp.is_defined (this.finished_time) && this.finished_time < this.started_time) { return false; } return true; } public inline bool is_started () { return Ft.Timestamp.is_defined (this.started_time); } public inline bool is_running () { return Ft.Timestamp.is_defined (this.started_time) && Ft.Timestamp.is_undefined (this.paused_time) && Ft.Timestamp.is_undefined (this.finished_time); } public inline bool is_paused () { return Ft.Timestamp.is_defined (this.paused_time) && Ft.Timestamp.is_defined (this.started_time) && Ft.Timestamp.is_undefined (this.finished_time); } public inline bool is_finished () { return Ft.Timestamp.is_defined (this.finished_time); } /** * Calculate elapsed time. * * It's only accurate when passing a current time. If you pass a historic time * the result will be just an estimate. */ public int64 calculate_elapsed (int64 timestamp) { if (Ft.Timestamp.is_undefined (this.started_time)) { return 0; } if (Ft.Timestamp.is_defined (this.paused_time)) { timestamp = int64.min (this.paused_time, timestamp); } if (Ft.Timestamp.is_defined (this.finished_time)) { timestamp = int64.min (this.finished_time, timestamp); } return ( timestamp - this.started_time - this.offset ).clamp (0, this.duration); } /** * Calculate remaining time. * * It's only accurate when passing a current time. If you pass a historic time * the result will be just an estimate. */ public int64 calculate_remaining (int64 timestamp) { return this.duration - this.calculate_elapsed (timestamp); } /** * Convert structure to Variant. * * Used in tests. */ public GLib.Variant to_variant () { var builder = new GLib.VariantBuilder (new GLib.VariantType ("a{s*}")); builder.add ("{sv}", "duration", new GLib.Variant.int64 (this.duration)); builder.add ("{sv}", "offset", new GLib.Variant.int64 (this.offset)); builder.add ("{sv}", "started_time", new GLib.Variant.int64 (this.started_time)); builder.add ("{sv}", "paused_time", new GLib.Variant.int64 (this.paused_time)); builder.add ("{sv}", "finished_time", new GLib.Variant.int64 (this.finished_time)); // builder.add ("{smh}", "user_data", this.user_data); return builder.end (); } /** * Represent state as string. * * Used in tests. */ public string to_representation () { var representation = new GLib.StringBuilder ("TimerState (\n"); representation.append (@" duration = $duration,\n"); representation.append (@" offset = $offset,\n"); representation.append (@" started_time = $started_time,\n"); representation.append (@" paused_time = $paused_time,\n"); representation.append (@" finished_time = $finished_time,\n"); representation.append (this.user_data == null ? " user_data = null\n" : " user_data = not null\n"); representation.append (")"); return representation.str; } } /** * Timer class mimics a physical countdown timer. * * It trigger events on state changes. To trigger ticking event at regular intervals use TimerTicker. */ public class Timer : GLib.Object { /** * Interval of the ticking signal. */ private const int64 TICKING_INTERVAL = Ft.Interval.SECOND; /** * Time that is within tolerance not to schedule a timeout. */ private const int64 TICKING_TOLERANCE = 20 * Ft.Interval.MILLISECOND; /** * Constraint for shorten/extend action to have some time remaining. */ private const int64 MIN_REMAINING_TIME_TO_SHORTEN = 10 * Ft.Interval.SECOND; private static Ft.Timer? instance = null; /** * Timer internal state. * * You should not change its fields directly. */ [CCode (notify = false)] public Ft.TimerState state { get { return this._state; } set { this.set_state_full (value); } } /** * The intended duration of the state, not counting gaps/interruptions. */ [CCode (notify = false)] public int64 duration { get { return this._state.duration; } set { this.set_duration_full (value); } } /** * Time when timer has been initialized/started. */ [CCode (notify = false)] public int64 started_time { get { return this._state.started_time; } } /** * Time lost during previous pauses. If pause is ongoing its not counted here yet. */ [CCode (notify = false)] public int64 offset { get { return this._state.offset; } } /** * Extra data associated with current state */ [CCode (notify = false)] public void* user_data { get { return this._state.user_data; } set { if (value == this._state.user_data) { return; } if (this.is_finished ()) { GLib.debug ("Trying to set timer user-data after it finished"); } var new_state = this._state.copy (); new_state.user_data = value; this.state = new_state; } } private Ft.TimerState _state = Ft.TimerState (); private uint timeout_id = 0; private int64 last_state_changed_time = Ft.Timestamp.UNDEFINED; private int64 last_tick_time = Ft.Timestamp.UNDEFINED; private int64 suspend_time = Ft.Timestamp.UNDEFINED; private bool resolving_state = false; private int changing_state = 0; private Ft.TimerState? state_to_resolve = null; private int64 monotonic_time_offset = 0; private Ft.SleepMonitor sleep_monitor; private ulong prepare_for_sleep_id = 0; private ulong woke_up_id = 0; construct { this.sleep_monitor = new Ft.SleepMonitor (); this.prepare_for_sleep_id = this.sleep_monitor.prepare_for_sleep.connect ( () => { this.suspend_time = this.get_current_time (); this.stop_timeout (this.suspend_time); this.suspending (this.suspend_time); } ); this.woke_up_id = this.sleep_monitor.woke_up.connect ( () => { var timestamp = Ft.Timestamp.from_now (); if (this.is_running ()) { this.synchronize (Ft.Timestamp.UNDEFINED, timestamp); } this.suspended (this.suspend_time, timestamp); this.suspend_time = Ft.Timestamp.UNDEFINED; } ); } public Timer (int64 duration = 0, void* user_data = null) requires (duration >= 0) { this._state = Ft.TimerState () { duration = duration, offset = 0, started_time = Ft.Timestamp.UNDEFINED, paused_time = Ft.Timestamp.UNDEFINED, finished_time = Ft.Timestamp.UNDEFINED, user_data = user_data }; } public Timer.with_state (Ft.TimerState state) { this._state = state; if (Ft.Timestamp.is_defined (this._state.started_time)) { this.last_state_changed_time = Ft.Timestamp.from_now (); this.start_timeout (this.last_state_changed_time); } } ~Timer () { if (Ft.Timer.instance == null) { Ft.Timer.instance = null; } } /** * Try to change state and update fields related to state change */ public void set_state_full (Ft.TimerState state, int64 timestamp = Ft.Timestamp.UNDEFINED) { this.ensure_timestamp (ref timestamp); var previous_state = this._state; this.resolve_state_internal (ref state, timestamp); if (!state.is_valid ()) { GLib.error ("Trying to set timer an invalid state: %s", state.to_representation ()); } if (this._state.equals (state)) { return; } this.last_state_changed_time = timestamp; this._state = state; this.changing_state++; // Reset internal ticking interval, so that it aligns to seconds after skipping or rewinding. if (state.started_time != previous_state.started_time || state.offset != previous_state.offset) { this.stop_timeout (timestamp); } this.notify_property ("state"); if (state.duration != previous_state.duration) { this.notify_property ("duration"); } if (state.started_time != previous_state.started_time) { this.notify_property ("started-time"); } if (state.offset != previous_state.offset) { this.notify_property ("offset"); } if (state.user_data != previous_state.user_data) { this.notify_property ("user-data"); } this.state_changed (this._state, previous_state); this.changing_state--; } public void set_duration_full (int64 duration, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (duration < 0) { GLib.debug ("Trying to set a negative timer duration (%.1fs).", Ft.Timestamp.to_seconds (duration)); duration = 0; } if (duration == this._state.duration) { return; } var new_state = this._state.copy (); new_state.duration = duration; if (new_state.duration > this._state.duration) { new_state.finished_time = Ft.Timestamp.UNDEFINED; } this.set_state_full (new_state, timestamp); } /** * Resolve timer state. * * It's a wrapper for `this.resolve_state`, handling a possible recursion. */ private void resolve_state_internal (ref Ft.TimerState state, int64 timestamp) { var recursion_count = 0; if (this.resolving_state) { this.state_to_resolve = state; return; } if (this._state.equals (state)) { return; } this.resolving_state = true; while (true) { this.resolve_state (ref state, timestamp); if (this.state_to_resolve == null) { break; } if (recursion_count > 1) { GLib.error ("Reached recursion limit while resolving timer state"); } state = this.state_to_resolve; this.state_to_resolve = null; recursion_count++; } this.resolving_state = false; } /** * Sets a default `Timer`. * * The old default timer is unreffed and the new timer referenced. * * A value of null for this will cause the current default timer to be released and a new default timer * to be created on demand. */ public static void set_default (Ft.Timer? timer) { Ft.Timer.instance = timer; } /** * Return a default timer. * * A new default timer will be created on demand. */ public static unowned Ft.Timer get_default () { if (Ft.Timer.instance == null) { Ft.Timer.set_default (new Ft.Timer ()); } return Ft.Timer.instance; } public bool is_default () { return Ft.Timer.instance == this; } /** * Return whether timer is ticking -- whether timer has started, is not paused and hasn't finished. */ public bool is_running () { return this._state.is_running (); } /** * Return whether timer has been started. */ public bool is_started () { return this._state.is_started (); } /** * Return whether timer is paused. */ public bool is_paused () { return this._state.is_paused (); } /** * Return whether timer has finished. * * It does not need to reach full time for timer to be marked as finished. */ public bool is_finished () { return this._state.is_finished (); } /** * Reset timer to initial state. */ public void reset (int64 duration = 0, void* user_data = null, int64 timestamp = Ft.Timestamp.UNDEFINED) requires (duration >= 0) { this.set_state_full ( Ft.TimerState () { duration = duration, offset = 0, started_time = Ft.Timestamp.UNDEFINED, paused_time = Ft.Timestamp.UNDEFINED, finished_time = Ft.Timestamp.UNDEFINED, user_data = user_data }, timestamp ); } /** * Start the timer or continue where it left off. */ public void start (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this.is_started () || this.is_finished ()) { return; } this.ensure_timestamp (ref timestamp); var new_state = this._state.copy (); new_state.started_time = timestamp; new_state.paused_time = Ft.Timestamp.UNDEFINED; this.set_state_full (new_state, timestamp); } /** * Stop the timer if it's running. */ public void pause (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this.is_paused () || !this.is_started () || this.is_finished ()) { return; } this.ensure_timestamp (ref timestamp); var new_state = this._state.copy (); new_state.paused_time = this.round_seconds (timestamp); this.set_state_full (new_state, timestamp); } /** * Resume timer if paused. */ public void resume (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (!this.is_started () || this.is_finished () || !this.is_paused ()) { return; } this.ensure_timestamp (ref timestamp); var new_state = this._state.copy (); new_state.offset += timestamp - new_state.paused_time; new_state.paused_time = Ft.Timestamp.UNDEFINED; this.set_state_full (new_state, timestamp); } /** * Rewind */ public void rewind (int64 interval, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (!this.is_started ()) { return; } if (interval == 0) { return; } if (interval < 0) { GLib.debug ("Rewinding timer with negative value (%.1fs).", Ft.Timestamp.to_seconds (interval)); } this.ensure_timestamp (ref timestamp); var elapsed = this.calculate_elapsed (timestamp); var new_elapsed = Ft.Timestamp.round (int64.max (elapsed - interval, 0), TICKING_INTERVAL); var new_state = this._state.copy (); new_state.finished_time = Ft.Timestamp.UNDEFINED; if (Ft.Timestamp.is_defined (new_state.paused_time)) { new_state.offset = new_state.paused_time - new_state.started_time - new_elapsed; } else { new_state.offset = timestamp - new_state.started_time - new_elapsed; } this.set_state_full (new_state, timestamp); } /** * Extend */ public void extend (int64 interval, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this.duration <= 0) { return; } if (interval == 0) { return; } this.ensure_timestamp (ref timestamp); var elapsed = this.calculate_elapsed (timestamp); var remaining = this._state.duration - elapsed; remaining = remaining + interval >= MIN_REMAINING_TIME_TO_SHORTEN ? remaining + interval : MIN_REMAINING_TIME_TO_SHORTEN; this.set_duration_full (elapsed + remaining, timestamp); } /** * Mark state as "finished". * * Do not use it. It's public only for unit-tests. */ public void finish (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this.is_finished ()) { return; } this.ensure_timestamp (ref timestamp); this.stop_timeout (timestamp); var new_state = this._state.copy (); new_state.finished_time = timestamp; if (Ft.Timestamp.is_defined (new_state.paused_time)) { new_state.offset += timestamp - new_state.paused_time; new_state.paused_time = Ft.Timestamp.UNDEFINED; } this.set_state_full (new_state, timestamp); } public void synchronize (int64 monotonic_time = Ft.Timestamp.UNDEFINED, int64 real_time = Ft.Timestamp.UNDEFINED) { if (Ft.Timestamp.is_undefined (real_time)) { real_time = Ft.Timestamp.from_now (); } if (Ft.Timestamp.is_undefined (monotonic_time)) { monotonic_time = GLib.get_monotonic_time (); } this.monotonic_time_offset = real_time - monotonic_time; this.synchronized (); } /** * Return approximate real time. */ public int64 get_current_time (int64 monotonic_time = Ft.Timestamp.UNDEFINED) { if (this.changing_state > 0 && Ft.Timestamp.is_undefined (monotonic_time)) { return this.last_state_changed_time; } if (this.monotonic_time_offset == 0 || Ft.Timestamp.is_frozen ()) { return Ft.Timestamp.from_now (); } if (Ft.Timestamp.is_undefined (monotonic_time)) { monotonic_time = GLib.get_monotonic_time (); } return monotonic_time + this.monotonic_time_offset; } private int64 round_seconds (int64 timestamp) { if (this.last_tick_time > 0 && (timestamp - this.last_tick_time).abs () < 2 * TICKING_INTERVAL) { return this.last_tick_time; } if (Ft.Timestamp.is_defined (this._state.started_time)) { var elapsed = this.calculate_elapsed (timestamp); var elapsed_rounded = Ft.Timestamp.round (elapsed, TICKING_INTERVAL); return this._state.started_time + this._state.offset + elapsed_rounded; } return timestamp; } private inline void ensure_timestamp (ref int64 timestamp) { if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = this.get_current_time (); } } private int64 calculate_tick_time (int64 timestamp) { var elapsed = this.calculate_elapsed (timestamp); var elapsed_rounded = Ft.Timestamp.round (elapsed, TICKING_INTERVAL); return Ft.Timestamp.is_defined (this._state.started_time) ? this._state.started_time + this._state.offset + elapsed_rounded : Ft.Timestamp.UNDEFINED; } /** * Ticking timeout. * * Unfortunately it can deviate from full seconds. */ private bool on_timeout () requires (this.timeout_id != 0) { var timestamp = this.get_current_time (GLib.MainContext.current_source ().get_time ()); var timestamp_rounded = this.calculate_tick_time (timestamp); var remaining = this.calculate_remaining (timestamp); if (remaining > 0 && this.last_tick_time != timestamp_rounded) { this.last_tick_time = timestamp_rounded; this.tick (timestamp_rounded); } // Check if already finished. if (remaining < TICKING_TOLERANCE) { this.timeout_id = 0; this.finish (timestamp); return GLib.Source.REMOVE; } // Check whether to switch to a more precise timeout. if (remaining < TICKING_INTERVAL + TICKING_TOLERANCE) { this.timeout_id = GLib.Timeout.add (Ft.Timestamp.to_milliseconds_uint (remaining), this.on_timeout_once); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Timer.on_timeout_once"); return GLib.Source.REMOVE; } return GLib.Source.CONTINUE; } /** * Precise timeout. * * It's meant to set up idle timeout that is aligned to full seconds. */ private bool on_timeout_once () requires (this.timeout_id != 0) { var timestamp = this.get_current_time (GLib.MainContext.current_source ().get_time ()); var timestamp_rounded = this.calculate_tick_time (timestamp); var remaining = this.calculate_remaining (timestamp); this.timeout_id = 0; if (remaining > 0 && this.last_tick_time != timestamp_rounded) { this.last_tick_time = timestamp_rounded; this.tick (timestamp_rounded); } // Check if already finished. if (remaining < TICKING_TOLERANCE) { this.finish (timestamp); return GLib.Source.REMOVE; } // Close to finish. Schedule one more timeout instead of interval. if (remaining < TICKING_INTERVAL + TICKING_TOLERANCE) { this.timeout_id = GLib.Timeout.add (Ft.Timestamp.to_milliseconds_uint (remaining), this.on_timeout_once); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Timer.on_timeout_once"); return GLib.Source.REMOVE; } // Schedule ticking at regular interval. this.timeout_id = GLib.Timeout.add (Ft.Timestamp.to_milliseconds_uint (TICKING_INTERVAL), this.on_timeout); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Timer.on_timeout"); return GLib.Source.REMOVE; } /** * Start ticking. First tick will be emitted after the `interval`. * * `aligned` will align ticks to elapsed time. It's meant for displaying elapsed/remaining time to sync label * updates with the timer. */ private void start_timeout_internal (int64 timestamp) requires (Ft.Timestamp.is_defined (timestamp)) requires (this.timeout_id == 0) { var timestamp_rounded = this.calculate_tick_time (timestamp); var remaining = this.calculate_remaining (timestamp); this.last_tick_time = timestamp_rounded; if (remaining > TICKING_TOLERANCE) { var deviation = timestamp - timestamp_rounded; if (remaining > TICKING_INTERVAL && deviation.abs () < TICKING_TOLERANCE) { this.timeout_id = GLib.Timeout.add ( Ft.Timestamp.to_milliseconds_uint (TICKING_INTERVAL), this.on_timeout); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Timer.on_timeout"); } else { this.timeout_id = GLib.Timeout.add ( Ft.Timestamp.to_milliseconds_uint (TICKING_INTERVAL - deviation), this.on_timeout_once); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Timer.on_timeout_once"); } } else { this.finish (timestamp); } } private void start_timeout (int64 timestamp) { if (this.timeout_id != 0 ) { return; // already running } if (Ft.Timestamp.is_frozen ()) { return; // don't run timeout in unittests } this.synchronize (Ft.Timestamp.UNDEFINED, timestamp); this.start_timeout_internal (timestamp); } private void stop_timeout (int64 timestamp) { // State may change right before a timeout callback gets called. // Ensure that tick gets emitted if it's delayed. var timestamp_rounded = this.calculate_tick_time (timestamp); if (this.last_tick_time != timestamp_rounded && timestamp_rounded >= this.last_state_changed_time) { this.last_tick_time = timestamp_rounded; this.tick (timestamp_rounded); } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.monotonic_time_offset = 0; this.last_tick_time = Ft.Timestamp.UNDEFINED; } /** * Return time of a last state change. * * It's deliberate that "last_state_changed_time" is not a property, as we don't want emitting * notify events for that. */ public int64 get_last_state_changed_time () { return this.last_state_changed_time; } public int64 get_last_tick_time () { return this.last_tick_time; } /** * Calculate elapsed time. * * It's only accurate when passing a current time. If you pass a historic time * the result will be just an estimate. */ public int64 calculate_elapsed (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (Ft.Timestamp.is_undefined (this._state.started_time)) { return 0; } this.ensure_timestamp (ref timestamp); return this._state.calculate_elapsed (timestamp); } /** * Calculate remaining time. * * It's only accurate when passing a current time. If you pass a historic time * the result will be just an estimate. */ public int64 calculate_remaining (int64 timestamp = Ft.Timestamp.UNDEFINED) { return this._state.duration - this.calculate_elapsed (timestamp); } /** * Calculate progress. * * It's only accurate when passing a current time. If you pass a historic time * the result will be just an estimate. */ public double calculate_progress (int64 timestamp = Ft.Timestamp.UNDEFINED) { var elapsed = (double) this.calculate_elapsed (timestamp); var duration = (double) this._state.duration; return duration > 0.0 ? elapsed / duration : 0.0; } /** * Calculate finish time. * * When paused it returns `Timestamp.UNDEFINED`. */ public int64 calculate_finish_time () { if (!this.is_running ()) { return Ft.Timestamp.UNDEFINED; } if (this.is_finished ()) { return this._state.finished_time; } return this._state.started_time + this._state.offset + this._state.duration; } /** * Wait until timer finishes * * It will only have an effect after Timer.start() or after setting up timer state. * * Intended for unit tests. */ public void run (GLib.Cancellable? cancellable = null) { var main_context = GLib.MainContext.@default (); while (this.is_running () && (cancellable == null || !cancellable.is_cancelled ())) { main_context.iteration (true); } } /** * Manually perform a tick or check whether timer has finished. * * Intended for unit tests. */ public void iterate () { var timestamp = Ft.Timestamp.peek (); var timestamp_rounded = this.calculate_tick_time (timestamp); var remaining = this.calculate_remaining (timestamp); if (remaining > 0 && this.last_tick_time != timestamp_rounded) { this.tick (timestamp_rounded); } if (remaining < TICKING_TOLERANCE) { this.finish (timestamp); } } /** * Emitted before setting a new state. * * It allows for fine-tuning the state before emitting state-changed signal. * Default handler ensures that state is valid. */ public signal void resolve_state (ref Ft.TimerState state, int64 timestamp) { if (Ft.Timestamp.is_undefined (state.started_time)) { state.paused_time = Ft.Timestamp.UNDEFINED; state.finished_time = Ft.Timestamp.UNDEFINED; } if (Ft.Timestamp.is_defined (state.paused_time) && state.paused_time < state.started_time) { state.paused_time = state.started_time; } if (Ft.Timestamp.is_defined (state.finished_time) && state.finished_time < state.started_time) { state.finished_time = state.started_time; } } /** * Emitted on any state related changes. Default handler acknowledges the change. */ public signal void state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { if (current_state.is_running ()) { this.start_timeout (this.last_state_changed_time); } else { this.stop_timeout (this.last_state_changed_time); } if (current_state.is_finished () && previous_state.is_started () && !previous_state.is_finished ()) { this.finished (current_state); } } /** * Emitted every second when timer is running. * * Ticks are aligned to full seconds of elapsed time. */ [Signal (run = "first")] public signal void tick (int64 timestamp) { this.last_tick_time = timestamp; } /** * Emitted when countdown is close to zero or passed it. */ public signal void finished (Ft.TimerState state); /** * Emitted after synchronizing timer against monotonic time. */ public signal void synchronized (); /** * Emitted before system has suspended. * * It's emitted even when timer is not running. */ public signal void suspending (int64 start_time); /** * Emitted right after system wakes up and the timer has been synchronised. * * It's emitted even when the timer is not running. */ public signal void suspended (int64 start_time, int64 end_time) { if (this.last_state_changed_time > start_time) { // Signal handler pushed a new state. return; } if (this.is_running ()) { var new_state = this._state.copy (); new_state.offset += end_time - start_time; this.set_state_full (new_state, end_time); } } public override void dispose () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } if (this.prepare_for_sleep_id != 0) { this.sleep_monitor.disconnect (this.prepare_for_sleep_id); this.prepare_for_sleep_id = 0; } if (this.woke_up_id != 0) { this.sleep_monitor.disconnect (this.woke_up_id); this.woke_up_id = 0; } this.sleep_monitor = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/timestamp.vala000066400000000000000000000231251520625676500232510ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ // TODO: move to interval.vala /** * Helper functions for handling duration. */ namespace Ft.Interval { public const int64 MICROSECOND = 1; public const int64 MILLISECOND = 1000; public const int64 SECOND = 1000000; public const int64 MINUTE = 60 * SECOND; public const int64 HOUR = 60 * MINUTE; public const int64 DAY = 24 * HOUR; public const int64 MIN = int64.MIN; public const int64 MAX = int64.MAX; public int64 from_value (int value, int64 unit) { return unit * value; } public int64 from_seconds (double seconds) { return (int64) Math.floor (seconds * (double) Ft.Interval.SECOND); } public int64 add (int64 interval, int64 other) { // TODO: use hardware acceleration for handling overflow https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html if (other > 0) { return interval < MAX - other ? interval + other : MAX; } else { return interval > MIN - other ? interval + other : MIN; } } public int64 subtract (int64 interval, int64 other) { return add (interval, -other); } public int64 round (int64 interval, int64 unit) { var unit_half = unit / 2; var remainder = interval % unit; if (remainder > unit_half) { return interval - remainder + unit; } if (remainder < -unit_half) { return interval - remainder - unit; } return interval - remainder; } public int64 round_seconds (int64 interval) { return round (interval, Ft.Interval.SECOND); } public double to_seconds (int64 interval) { return ((double) interval) / ((double) Ft.Interval.SECOND); } public string format_short (int64 interval, int64 unit = -1) { var seconds = (int) (interval / Ft.Interval.SECOND); var hours = 0; var minutes = 0; var show_hours = false; var show_minutes = false; var result = new GLib.StringBuilder (); if (unit == Ft.Interval.HOUR) { hours = seconds / 3600; show_hours = true; } else if (unit == Ft.Interval.MINUTE) { minutes = seconds / 60; show_minutes = true; } else { hours = seconds / 3600; if (hours < 0) { seconds = seconds.abs (); } minutes = (seconds % 3600) / 60; show_hours = hours != 0; show_minutes = minutes != 0 || hours == 0; } if (show_hours) { /* translators: Short form for number of hours */ result.append_printf (_("%uh"), hours); } if (show_hours && show_minutes) { result.append ("\u00A0"); // non-breaking space } if (show_minutes) { /* translators: Short form for number of minutes */ result.append_printf (_("%um"), minutes); } return result.str; } } /** * Helper functions for handling time. */ namespace Ft.Timestamp { // Special value indicating that timestamp is not set. Assume that timestamps do not go below 0. public const int64 UNDEFINED = -1; // Value range of a timestamp. public const int64 MIN = 0; public const int64 MAX = int64.MAX; private int64 current_time = UNDEFINED; private int64 advance_by = 0; public int64 from_now () { if (current_time >= 0) { var tmp = current_time; current_time += advance_by; return tmp; } return GLib.get_real_time (); } public int64 from_seconds (double seconds) { return (int64) Math.round (seconds * (double) Ft.Interval.SECOND); } public int64 from_seconds_uint (uint seconds) { return (int64) seconds * Ft.Interval.SECOND; } public int64 from_milliseconds_uint (uint milliseconds) { return (int64) milliseconds * Ft.Interval.MILLISECOND; } public int64 from_iso8601 (string text) { var datetime = new GLib.DateTime.from_iso8601 (text, null); return datetime != null ? datetime.to_unix () * Ft.Interval.SECOND + datetime.get_microsecond () : Ft.Timestamp.UNDEFINED; } public int64 from_datetime (GLib.DateTime datetime) { return datetime.to_unix () * Ft.Interval.SECOND; } public double to_seconds (int64 timestamp) { return ((double) timestamp) / ((double) Ft.Interval.SECOND); } public uint to_seconds_uint (int64 timestamp) { return (uint) (timestamp / Ft.Interval.SECOND).clamp (0, uint.MAX); } public uint to_seconds_uint32 (int64 timestamp) { return (uint32) (timestamp / Ft.Interval.SECOND).clamp (0, uint32.MAX); } public double to_milliseconds (int64 timestamp) { return ((double) timestamp) / ((double) Ft.Interval.MILLISECOND); } public uint to_milliseconds_uint (int64 timestamp) { return (uint) (timestamp / Ft.Interval.MILLISECOND).clamp (0, uint.MAX); } public GLib.DateTime? to_datetime (int64 timestamp, GLib.TimeZone? timezone = null) { if (is_undefined (timestamp)) { return null; } var datetime = new GLib.DateTime.from_unix_utc (timestamp / Ft.Interval.SECOND); return timezone != null ? datetime.to_timezone (timezone) : datetime.to_local (); } public string to_iso8601 (int64 timestamp) { if (timestamp < 0) { return ""; } var seconds = timestamp / Ft.Interval.SECOND; var microseconds = timestamp % Ft.Interval.SECOND; var datetime_string = (new GLib.DateTime.from_unix_utc (seconds)).format_iso8601 (); // TODO: do we really need UTC, local may be prefferable? if (microseconds > 0) { datetime_string = datetime_string.splice (-1, -1, microseconds.to_string (@".%06$(int64.FORMAT)")); } return datetime_string; } public inline bool is_defined (int64 timestamp) { return timestamp >= Ft.Timestamp.MIN; } public inline bool is_undefined (int64 timestamp) { return timestamp < Ft.Timestamp.MIN; } public int64 add_interval (int64 timestamp, int64 interval) { // TODO: use hardware acceleration for handling overflow https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html if (is_undefined (timestamp)) { return UNDEFINED; } if (interval >= 0) { return interval < MAX - timestamp ? timestamp + interval : MAX; } else { return -interval < MIN + timestamp ? timestamp + interval : MIN; } } /** * Subtract two timestamps. The result is an interval. */ public int64 subtract (int64 timestamp, int64 other) { // TODO: use hardware acceleration for handling overflow https://gcc.gnu.org/onlinedocs/gcc/Integer-Overflow-Builtins.html if (is_undefined (timestamp)) { return 0; } if (is_undefined (other)) { return 0; } return timestamp - other; } public int64 subtract_interval (int64 timestamp, int64 interval) { return add_interval (timestamp, -interval); } public int64 round (int64 timestamp, int64 unit) { return is_defined (timestamp) ? Ft.Interval.round (timestamp, unit) : Ft.Timestamp.UNDEFINED; } public int64 round_seconds (int64 timestamp) { return round (timestamp, Ft.Interval.SECOND); } /* * Functions for unit tests */ /** * Freeze `Ft.Timestamp.from_now()` to current time. Used in unittests. */ public int64 freeze () { if (Ft.Timestamp.is_undefined (current_time)) { current_time = Ft.Timestamp.from_now (); } return current_time; } /** * Freeze `Ft.Timestamp.from_now()` to a given value. Used in unittests. */ public void freeze_to (int64 timestamp) { current_time = timestamp; } /** * Revert `freeze()` call. Used in unittests. */ public void thaw () { current_time = Ft.Timestamp.UNDEFINED; } public bool is_frozen () { return Ft.Timestamp.is_defined (current_time); } /** * Return current time if frozen. */ public int64 peek () { return current_time; } /** * Advance frozen time. Used in unittests. */ public int64 advance (int64 interval) requires (interval >= 0) { if (!is_frozen ()) { Ft.Timestamp.freeze (); } current_time += interval; return current_time; } /** * If frozen, make every call `Ft.Timestamp.from_now ()` advance by given interval. Used in unittests. */ public void set_auto_advance (int64 interval) requires (interval >= 0) { advance_by = interval; } } focustimerhq-FocusTimer-8581be2/src/core/timezone-entry.vala000066400000000000000000000010451520625676500242340ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { private class TimezoneEntry : Gom.Resource { public int64 id { get; set; } public int64 time { get; set; } public string identifier { get; set; } static construct { set_table ("timezones"); set_primary_key ("id"); set_unique ("time"); set_notnull ("time"); set_notnull ("identifier"); } } } focustimerhq-FocusTimer-8581be2/src/core/timezone-history.vala000066400000000000000000000330401520625676500245740ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * A lightweight equivalent of `TimezoneEntry` */ [Compact] private class TimezoneMarker { public int64 timestamp; public GLib.TimeZone timezone; public TimezoneMarker (int64 timestamp, GLib.TimeZone timezone) { this.timestamp = timestamp; this.timezone = timezone; } ~TimezoneMarker () { this.timezone = null; } } public delegate void TimezoneScanFunc (int64 start_time, int64 end_time, GLib.TimeZone timezone); /** * We use UTC timestamp throughout the app and store local timezone separately * as a way to convert it to local time when needed. * * Internally, data is stored from most recent to oldest entries. Be aware that * `GLib.TimeZone` timestamps are in seconds while ours are in microseconds. */ [SingleInstance] public class TimezoneHistory : GLib.Object { private const uint FETCH_LIMIT = 50; private GLib.Array data; // TODO: consider a linked list private int64 fetched_timestamp = Ft.Timestamp.UNDEFINED; private bool fetched_all = false; construct { this.data = new GLib.Array (); } private inline Ft.TimezoneMarker create_marker (int64 timestamp, GLib.TimeZone timezone) { return new Ft.TimezoneMarker (timestamp, timezone); } /** * Try to fill in `data`. * * Returns the number of fetched entries. */ private uint fetch () { Gom.Filter? filter = null; if (this.fetched_all) { return 0U; } if (Ft.Timestamp.is_defined (this.fetched_timestamp)) { filter = new Gom.Filter.lt (typeof (Ft.TimezoneEntry), "time", this.fetched_timestamp); } var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.TimezoneEntry), "time", Gom.SortingMode.DESCENDING); var repository = Ft.Database.get_repository (); try { var results = repository.find_sorted_sync (typeof (Ft.TimezoneEntry), filter, sorting); var results_count = results.count; var fetch_count = uint.min (results_count, FETCH_LIMIT); results.fetch_sync (0, fetch_count); for (var index = 0; index < fetch_count; index++) { var entry = (Ft.TimezoneEntry) results.get_index (index); this.data.append_val (this.create_marker ( entry.time, new GLib.TimeZone.identifier (entry.identifier))); this.fetched_timestamp = entry.time; } if (results_count < FETCH_LIMIT) { this.fetched_all = true; } return fetch_count; } catch (GLib.Error error) { GLib.warning ("Failed to fetch timezones: %s", error.message); } return 0U; } private bool replace_in_database (int64 timestamp, GLib.TimeZone timezone) { var timestamp_value = GLib.Value (typeof (int64)); timestamp_value.set_int64 (timestamp); var repository = Ft.Database.get_repository (); var filter = new Gom.Filter.eq ( typeof (Ft.TimezoneEntry), "time", timestamp_value); try { var entry = (Ft.TimezoneEntry?) repository.find_one_sync ( typeof (Ft.TimezoneEntry), filter); if (entry == null) { return false; } entry.identifier = timezone.get_identifier (); entry.save_sync (); } catch (GLib.Error error) { return false; } return true; } public void insert (int64 timestamp, GLib.TimeZone timezone) requires (Ft.Timestamp.is_defined (timestamp)) { unowned Ft.TimezoneMarker? existing_marker; uint index; this.search_internal (timestamp, out existing_marker, out index); if (existing_marker != null) { if (existing_marker.timezone.get_identifier () == timezone.get_identifier ()) { return; // avoid inserting duplicates } if (existing_marker.timestamp == timestamp) { this.data.remove_index (index); // replacing existing marker } this.data.insert_val (index, this.create_marker (timestamp, timezone)); } else { this.data.append_val (this.create_marker (timestamp, timezone)); } var entry = new Ft.TimezoneEntry (); entry.repository = Ft.Database.get_repository (); entry.time = timestamp; entry.identifier = timezone.get_identifier (); try { entry.save_sync (); this.changed (); } catch (GLib.Error error) { if (!this.replace_in_database (timestamp, timezone)) { GLib.warning ("Failed to save timezone %s at %s: %s", timezone.get_identifier (), timestamp.to_string (), error.message); } } } private inline void search_internal (int64 timestamp, out unowned Ft.TimezoneMarker? marker, out uint index) { marker = null; index = 0U; var _index = index; do { while (_index < this.data.length) { unowned var _marker = this.data.index (_index); if (timestamp >= _marker.timestamp) { marker = _marker; index = _index; return; } _index++; } } while (this.fetch () > 0); } public unowned GLib.TimeZone? search (int64 timestamp) { unowned Ft.TimezoneMarker? marker; this.search_internal (timestamp, out marker, null); return marker?.timezone; } public unowned GLib.TimeZone? search_by_date (GLib.Date date, int64 offset = 0) { unowned Ft.TimezoneMarker? marker = null; unowned Ft.TimezoneMarker? last_valid_marker = null; uint index; var estimated_datetime = new GLib.DateTime.utc ( date.get_year (), date.get_month (), date.get_day (), 0, 0, 0); estimated_datetime.add_days (-2); var estimated_timestamp = estimated_datetime.to_unix (); this.search_internal (estimated_timestamp, out marker, out index); if (marker == null && this.data.length > 0) { index = this.data.length - 1; marker = this.data.index (index); } while (marker != null) { var datetime = new GLib.DateTime ( marker.timezone, date.get_year (), date.get_month (), date.get_day (), 0, 0, 0); if (offset != 0) { datetime = datetime.add_seconds (Ft.Interval.to_seconds (offset)); } if (marker.timestamp > datetime.to_unix ()) { break; } last_valid_marker = marker; index++; marker = index < this.data.length ? this.data.index (index) : null; } return last_valid_marker?.timezone; } /** * Try to find fist occurrence of an timezone offset change. * It only considers one such occurrence for a given time range. For our purposes * it's good enough - we need it to be reliable up to a day, preferably up to a month. */ private inline void split_timezone (int64 start_time, int64 end_time, GLib.TimeZone timezone, Ft.TimezoneScanFunc func) { var start_interval_id = timezone.find_interval ( GLib.TimeType.UNIVERSAL, start_time / Ft.Interval.SECOND); var end_interval_id = timezone.find_interval ( GLib.TimeType.UNIVERSAL, end_time / Ft.Interval.SECOND); var start_offset = timezone.get_offset (start_interval_id); var end_offset = timezone.get_offset (end_interval_id); if (start_offset != end_offset) { // Use binary search for finding the transition time. var range_start_time = start_time / Ft.Interval.SECOND; var range_end_time = end_time / Ft.Interval.SECOND; var range_mid_time = range_start_time; var range_mid_offset = start_offset; while (range_start_time < range_end_time) { range_mid_time = range_start_time + (range_end_time - range_start_time) / 2; range_mid_offset = timezone.get_offset ( timezone.find_interval (GLib.TimeType.UNIVERSAL, range_mid_time)); if (range_mid_offset != start_offset) { range_end_time = range_mid_time; } else { range_start_time = range_mid_time + 1; } } // Round 59:59 to a full minutes to make it consistent with our time-blocks API. // `range_end - range_start` in should return correct range duration. if (range_mid_time % 1800 == 1799) { range_mid_time += 1; } var split_time = range_mid_time * Ft.Interval.SECOND; if (start_time < split_time) { func (start_time, split_time, timezone); } if (split_time < end_time) { func (split_time, end_time, timezone); } } else { func (start_time, end_time, timezone); } } public void scan (int64 start_time, int64 end_time, Ft.TimezoneScanFunc func) { if (start_time > end_time) { return; } unowned Ft.TimezoneMarker? marker; unowned Ft.TimezoneMarker? next_marker; uint index; this.search_internal (start_time, out marker, out index); if (marker == null && !Ft.is_test ()) // XXX: avoid `is_test`; specify a fallback-timezone { func (start_time, marker != null ? marker.timestamp : end_time, new GLib.TimeZone.local ()); return; } while (marker != null && marker.timestamp < end_time) { next_marker = index >= 1 ? this.data.index (index - 1U) : null; this.split_timezone ( int64.max (marker.timestamp, start_time), next_marker != null ? int64.min (next_marker.timestamp, end_time) : end_time, marker.timezone, func); if (next_marker != null) { marker = next_marker; index--; } else { break; } } } public void clear_cache () { this.data = new GLib.Array (); this.fetched_timestamp = Ft.Timestamp.UNDEFINED; this.fetched_all = false; } public signal void changed (); public override void dispose () { this.data = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/core/timezone-monitor.vala000066400000000000000000000046411520625676500245670ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public interface TimeZoneMonitorProvider : Ft.Provider { public abstract string? identifier { get; } } [SingleInstance] public class TimeZoneMonitor : Ft.ProvidedObject { public GLib.TimeZone timezone { get { return this._timezone; } } private GLib.TimeZone? _timezone; private void update_timezone (string? timezone_identifier) { GLib.TimeZone? timezone = null; if (timezone_identifier != null) { try { timezone = new GLib.TimeZone.identifier (timezone_identifier); } catch (GLib.Error error) { GLib.warning ("Could not find timezone \"%s\": %s", timezone_identifier, error.message); timezone = new GLib.TimeZone.local (); } } else { timezone = new GLib.TimeZone.local (); } if (this._timezone == null || this._timezone.get_identifier () != timezone.get_identifier ()) { this._timezone = timezone; this.notify_property ("timezone"); this.changed (); } } private void on_notify_identifier (GLib.Object object, GLib.ParamSpec pspec) { var provider = (Ft.TimeZoneMonitorProvider) object; this.update_timezone (provider.identifier); } protected override void initialize () { this._timezone = new GLib.TimeZone.local (); } protected override void setup_providers () { } protected override void provider_enabled (Ft.TimeZoneMonitorProvider provider) { provider.notify["identifier"].connect (this.on_notify_identifier); this.update_timezone (provider.identifier); } protected override void provider_disabled (Ft.TimeZoneMonitorProvider provider) { provider.notify["identifier"].disconnect (this.on_notify_identifier); } public signal void changed (); } } focustimerhq-FocusTimer-8581be2/src/core/utils.vala000066400000000000000000000231521520625676500224060ustar00rootroot00000000000000/* * Copyright (c) 2013, 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { private static int _is_flatpak = -1; private static string? _desktop_name = null; public delegate bool FilterFunc (T item); public inline void ensure_timestamp (ref int64 timestamp) { if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = Ft.Timestamp.from_now (); } } public inline string ensure_string (string? str) { return str != null ? str : ""; } /** * Round seconds to 1s, 5s, 10s, 1m. * * Its intended for displaying rough estimation of duration. */ // XXX: rename, name conflicts with Interval.round_seconds public double round_seconds (double seconds) { if (seconds < 10.0) { return Math.round (seconds); } if (seconds < 30.0) { return 5.0 * Math.round (seconds / 5.0); } if (seconds < 60.0) { return 10.0 * Math.round (seconds / 10.0); } return 60.0 * Math.round (seconds / 60.0); } /** * Convert seconds to text. * * If hours are present, seconds are omitted. */ // XXX: rename to `format_seconds` ? public string format_time (uint seconds) // TODO: rename to format_interval { var hours = seconds / 3600; var minutes = (seconds % 3600) / 60; var str = ""; seconds = seconds % 60; if (hours > 0) { str = ngettext ("%u hour", "%u hours", hours).printf (hours); } if (minutes > 0) { if (str != "") { str += " "; } str += ngettext ("%u minute", "%u minutes", minutes).printf (minutes); } if ((seconds > 0 && hours == 0) || (seconds == 0 && minutes == 0 && hours == 0)) { if (str != "") { str += " "; } str += ngettext ("%u second", "%u seconds", seconds).printf (seconds); } return str; } public inline double lerp (double value_from, double value_to, double t) { return value_from + (value_to - value_from) * t; } public inline float lerpf (float value_from, float value_to, float t) { return value_from + (value_to - value_from) * t; } public bool is_flatpak () { if (_is_flatpak < 0) { var value = GLib.Environment.get_variable ("container") == "flatpak" && GLib.Environment.get_variable ("G_TEST_ROOT_PROCESS") == null; _is_flatpak = value ? 1 : 0; } return _is_flatpak > 0; } internal bool is_test () { return GLib.Environment.get_variable ("G_TEST_ROOT_PROCESS") != null || GLib.Environment.get_variable ("G_TEST_BUILDDIR") != null || GLib.Environment.get_variable ("MESON_TEST_ITERATION") != null; } public bool is_devel () { return Config.APPLICATION_ID.has_suffix (".Devel"); } public string get_desktop_name () { if (_desktop_name == null) { var desktop_name = GLib.Environment.get_variable ("XDG_SESSION_DESKTOP") ?? ""; if (desktop_name == "") { desktop_name = GLib.Environment.get_variable ("XDG_CURRENT_DESKTOP") ?? ""; } desktop_name = desktop_name.ascii_down (); switch (desktop_name) { case "ubuntu": desktop_name = "gnome"; break; case "xubuntu": desktop_name = "xfce"; break; case "lubuntu": desktop_name = "lxqt"; break; case "budgie-desktop": desktop_name = "budgie"; break; default: break; } _desktop_name = desktop_name; } return _desktop_name; } public string to_camel_case (string name) { var result = new GLib.StringBuilder (); var was_hyphen = false; unichar chr; int chr_span_end = 0; while (name.get_next_char (ref chr_span_end, out chr)) { if (chr == '-') { was_hyphen = true; continue; } if (was_hyphen) { was_hyphen = false; result.append_unichar (chr.toupper ()); } else { result.append_unichar (chr); } } return result.str; } public string from_camel_case (string name) { var result = new GLib.StringBuilder (); var was_lowercase = false; unichar chr; int chr_span_end = 0; while (name.get_next_char (ref chr_span_end, out chr)) { if (chr.isupper () && was_lowercase) { was_lowercase = false; result.append_c ('-'); result.append_unichar (chr.tolower ()); } else { was_lowercase = chr.islower (); result.append_unichar (was_lowercase ? chr : chr.tolower ()); } } return result.str; } public delegate void ExtensionSetForeachFunc (GLib.Object object); /** * Bindings for `Peas.ExtensionSet.@foreach` seem to be broken. * * Here's an simpler, Vala-friendly implementation. */ public void foreach_extension (Peas.ExtensionSet extension_set, ExtensionSetForeachFunc func) { var n_items = extension_set.get_n_items (); for (var i = 0U; i < n_items; i++) { var object = extension_set.get_item (i); func (object); } } /** * A convenience wrapper around GLib.Queue that provides an async wait() * which resolves in the main loop once the queue becomes empty. */ public class AsyncQueue { private GLib.Queue queue; private GLib.Mutex mutex; private GLib.Cond cond; private bool in_dispose = false; private GLib.GenericArray> waiting_threads; public AsyncQueue () { this.queue = new GLib.Queue (); this.mutex = GLib.Mutex (); this.cond = GLib.Cond (); this.waiting_threads = new GLib.GenericArray> (); } ~AsyncQueue () { this.mutex.lock (); this.in_dispose = true; this.cond.broadcast (); this.mutex.unlock (); foreach (var thread in this.waiting_threads) { thread.join (); } } /** * Push an item to the tail of the queue. */ public void push (owned T item) { this.mutex.lock (); this.queue.push_tail ((owned) item); this.mutex.unlock (); } /** * Try to pop an item without blocking. Returns null if empty. */ public T? pop () { this.mutex.lock (); T? item = null; if (this.queue.get_length () > 0) { item = this.queue.pop_head (); if (this.queue.get_length () == 0) { this.cond.broadcast (); } } this.mutex.unlock (); return item; } /** * Current length of the queue. */ public uint length () { this.mutex.lock (); var length = this.queue.get_length (); this.mutex.unlock (); return length; } /** * Asynchronously waits until the queue becomes empty. * * The completion is dispatched back to the main loop. */ public async void wait () { if (this.in_dispose) { return; } // Fast-path: already empty this.mutex.lock (); var already_empty = this.queue.get_length () == 0; this.mutex.unlock (); if (already_empty) { return; } SourceFunc callback = this.wait.callback; var thread = new GLib.Thread ( "async-queue-wait", () => { this.mutex.lock (); while (this.queue.get_length () > 0 && !this.in_dispose) { this.cond.wait (this.mutex); } this.mutex.unlock (); // Schedule the callback in the main loop var idle_source = new GLib.IdleSource (); idle_source.set_name ("Ft.AsyncQueue.wait"); idle_source.set_callback ((owned) callback); idle_source.attach (GLib.MainContext.@default ()); return true; }); this.mutex.lock (); this.waiting_threads.add (thread); this.mutex.unlock (); // Wait for the callback yield; this.mutex.lock (); this.waiting_threads.remove_fast (thread); this.mutex.unlock (); } } } focustimerhq-FocusTimer-8581be2/src/core/variables.vala000066400000000000000000000214261520625676500232200ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [CCode (has_target = false)] public delegate Ft.Value EvaluateFunc (Ft.Context context); public class VariableSpec { public string name; public string description; public GLib.Type value_type; public Ft.EvaluateFunc evaluate_func; public VariableSpec (string name, string description, GLib.Type value_type, owned Ft.EvaluateFunc evaluate_func) { this.name = name; this.description = description; this.value_type = value_type; this.evaluate_func = evaluate_func; } public inline Ft.Value evaluate (Ft.Context context) requires (this.evaluate_func != null) { return this.evaluate_func (context); } } private Ft.VariableSpec[] variable_specs = null; private GLib.HashTable variable_spec_by_name = null; namespace Variables { private Ft.Value get_timestamp (Ft.Context context) { return new Ft.TimestampValue (context.timestamp); } private Ft.StateValue get_state (Ft.Context context) { return new Ft.StateValue (context.time_block != null ? context.time_block.state : Ft.State.STOPPED); } private Ft.StatusValue get_status (Ft.Context context) { return new Ft.StatusValue (context.time_block != null ? context.time_block.get_status () : Ft.TimeBlockStatus.SCHEDULED); } private Ft.BooleanValue get_is_started (Ft.Context context) { return new Ft.BooleanValue (context.timer_state.is_started ()); } private Ft.BooleanValue get_is_paused (Ft.Context context) { return new Ft.BooleanValue (context.timer_state.is_paused ()); } private Ft.BooleanValue get_is_finished (Ft.Context context) { return new Ft.BooleanValue (context.timer_state.is_finished ()); } private Ft.BooleanValue get_is_running (Ft.Context context) { return new Ft.BooleanValue (context.timer_state.is_running ()); } private Ft.IntervalValue get_duration (Ft.Context context) { return new Ft.IntervalValue (context.timer_state.duration); } private Ft.IntervalValue get_offset (Ft.Context context) { return new Ft.IntervalValue (context.timer_state.offset); } private Ft.IntervalValue get_elapsed (Ft.Context context) { return new Ft.IntervalValue (context.timer_state.calculate_elapsed (context.timestamp)); } private Ft.IntervalValue get_remaining (Ft.Context context) { return new Ft.IntervalValue (context.timer_state.calculate_remaining (context.timestamp)); } private Ft.TimestampValue get_start_time (Ft.Context context) { return new Ft.TimestampValue (context.timer_state.started_time); } private void initialize () { variable_specs = new Ft.VariableSpec[0]; variable_spec_by_name = new GLib.HashTable (GLib.str_hash, GLib.str_equal); Ft.install_variable ( new Ft.VariableSpec ("timestamp", _("The exact time of the current event."), typeof (Ft.TimestampValue), get_timestamp)); Ft.install_variable ( new Ft.VariableSpec ("state", _("The current phase of the Pomodoro cycle. Possible values: stopped, pomodoro, break, short-break, long-break."), typeof (Ft.StateValue), get_state)); Ft.install_variable ( new Ft.VariableSpec ("status", _("Status of the current time-block. Possible values: scheduled, in-progress, completed, uncompleted."), typeof (Ft.StatusValue), get_status)); Ft.install_variable ( new Ft.VariableSpec ("is-started", _("A flag indicating whether countdown has begun."), typeof (Ft.BooleanValue), get_is_started)); Ft.install_variable ( new Ft.VariableSpec ("is-paused", _("A flag indicating whether countdown is paused."), typeof (Ft.BooleanValue), get_is_paused)); Ft.install_variable ( new Ft.VariableSpec ("is-finished", _("A flag indicating whether countdown has finished."), typeof (Ft.BooleanValue), get_is_finished)); Ft.install_variable ( new Ft.VariableSpec ("is-running", _("A flag indicating whether the timer is actively counting down."), typeof (Ft.BooleanValue), get_is_running)); Ft.install_variable ( new Ft.VariableSpec ("duration", _("Duration of the current countdown."), typeof (Ft.IntervalValue), get_duration)); Ft.install_variable ( new Ft.VariableSpec ("offset", // translators: Time difference between displayed value on the timer and real time. Think of it as a lost time. _("Discrepancy between elapsed time and the time passed."), typeof (Ft.IntervalValue), get_offset)); Ft.install_variable ( new Ft.VariableSpec ("elapsed", // translators: Time since the start of countdown _("The amount of time spent on the countdown."), typeof (Ft.IntervalValue), get_elapsed)); Ft.install_variable ( new Ft.VariableSpec ("remaining", // translators: Displayed timer value. _("The amount of time left before the countdown ends."), typeof (Ft.IntervalValue), get_remaining)); Ft.install_variable ( new Ft.VariableSpec ("start-time", _("Time when the countdown has started."), typeof (Ft.TimestampValue), get_start_time)); } internal inline void ensure_initialized () { if (variable_spec_by_name == null) { initialize (); } } } public void install_variable (Ft.VariableSpec variable_spec) { variable_specs += variable_spec; variable_spec_by_name.insert (variable_spec.name, variable_spec); } public unowned Ft.VariableSpec? find_variable (string variable_name) { Ft.Variables.ensure_initialized (); return variable_spec_by_name.lookup (variable_name); } public (unowned Ft.VariableSpec)[] list_variables () { Ft.Variables.ensure_initialized (); return variable_specs; } public bool find_variable_format (string variable_name, string variable_format) { if (variable_format == "") { return true; } var variable_spec = Ft.find_variable (variable_name); if (variable_spec != null) { foreach (var format in Ft.list_value_formats (variable_spec.value_type)) { if (variable_format == format) { return true; } } } return false; } } focustimerhq-FocusTimer-8581be2/src/dbus-services.vala000066400000000000000000000730541520625676500231020ustar00rootroot00000000000000/* * Copyright (c) 2012-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [DBus (name = "io.github.focustimerhq.FocusTimer")] public class ApplicationDBusService : GLib.Object { private const string DBUS_INTERFACE_NAME = "io.github.focustimerhq.FocusTimer"; public string version { owned get { return Config.PACKAGE_VERSION; } } [DBus (signature = "a{sv}")] public GLib.Variant settings { owned get { return this.serialized_settings; } } private weak GLib.DBusConnection? connection; private string object_path; private Ft.Application? application; private GLib.Settings? _settings; private GLib.Variant? serialized_settings = null; public ApplicationDBusService (GLib.DBusConnection connection, string object_path, Ft.Application application, GLib.Settings settings) { this.connection = connection; this.object_path = object_path; this.application = application; this._settings = settings; this.serialized_settings = this.serialize_settings (settings); settings.changed.connect (this.on_settings_changed); } private GLib.Variant serialize_settings (GLib.Settings settings) { var builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT); builder.add ( "{sv}", "announce-about-to-end", new GLib.Variant.boolean (this._settings.get_boolean ("announce-about-to-end"))); builder.add ( "{sv}", "screen-overlay", new GLib.Variant.boolean (this._settings.get_boolean ("screen-overlay"))); builder.add ( "{sv}", "screen-overlay-lock-delay", new GLib.Variant.uint32 (this._settings.get_uint ("screen-overlay-lock-delay"))); builder.add ( "{sv}", "screen-overlay-reopen-delay", new GLib.Variant.uint32 (this._settings.get_uint ("screen-overlay-reopen-delay"))); return builder.end (); } private void update_properties () { if (this.connection == null) { return; } var changed_properties = new GLib.VariantBuilder (GLib.VariantType.VARDICT); var invalidated_properties = new GLib.VariantBuilder (new GLib.VariantType ("as")); var serialized_settings = this.serialize_settings (this._settings); var changed = false; if (this.serialized_settings == null || !this.serialized_settings.equal (serialized_settings)) { this.serialized_settings = serialized_settings; changed_properties.add ("{sv}", "Settings", serialized_settings); changed = true; } if (changed) { try { this.connection.emit_signal ( null, this.object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", new GLib.Variant ( "(sa{sv}as)", DBUS_INTERFACE_NAME, changed_properties, invalidated_properties ) ); } catch (GLib.Error error) { GLib.warning ("Failed to emit PropertiesChanged signal: %s", error.message); } } } private void on_settings_changed (GLib.Settings settings, string key) { this.update_properties (); } public void show_window (string view) throws GLib.DBusError, GLib.IOError { this.application.show_window (Ft.WindowView.from_string (view)); } public void show_preferences (string view) throws GLib.DBusError, GLib.IOError { this.application.show_preferences (view); } public void quit () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("application.quit"); this.application.quit (); } [DBus (visible = false)] public void emit_request_focus () { this.request_focus (); } public signal void request_focus (); public override void dispose () { if (this._settings != null) { this._settings.changed.disconnect (this.on_settings_changed); this._settings = null; } this.serialized_settings = null; this.application = null; this.connection = null; base.dispose (); } } /** * Timer service provides equivalent functionality of the timer view in the app. */ [DBus (name = "io.github.focustimerhq.FocusTimer.Timer")] public class TimerDBusService : GLib.Object { private const string DBUS_INTERFACE_NAME = "io.github.focustimerhq.FocusTimer.Timer"; public string state { owned get { return this._state.to_string (); } set { var state = Ft.State.from_string (value); Ft.Context.set_event_source (@"session-manager.state:$(value)"); this.session_manager.advance_to_state (state); } } public int64 duration { get { return this.timer_state.duration; } set { if (this.timer.user_data != null) { this.timer.duration = value; } } } public int64 offset { get { return this.timer_state.offset; } } public int64 started_time { get { return this.timer_state.started_time; } } public int64 paused_time { get { return this.timer_state.paused_time; } } public int64 finished_time { get { return this.timer_state.finished_time; } } public int64 last_changed_time { get { return this.last_state_changed_time; } } private Ft.Timer? timer; private Ft.SessionManager? session_manager; private weak GLib.DBusConnection? connection; private string object_path; private Ft.State _state; private Ft.TimerState timer_state; private int64 last_state_changed_time = Ft.Timestamp.UNDEFINED; public TimerDBusService (GLib.DBusConnection connection, string object_path, Ft.Timer timer, Ft.SessionManager session_manager) { this.connection = connection; this.object_path = object_path; this.timer = timer; this.session_manager = session_manager; this.timer.state_changed.connect (this.on_timer_state_changed); this.timer.tick.connect (this.on_timer_tick); this.timer.finished.connect (this.on_timer_finished); this.session_manager.notify["current-state"].connect (this.on_session_manager_notify_current_state); } private void update_properties () { if (this.connection == null) { return; } var changed_properties = new GLib.VariantBuilder (GLib.VariantType.VARDICT); var invalidated_properties = new GLib.VariantBuilder (new GLib.VariantType ("as")); var timer_state = this.timer.state.copy (); var last_state_changed_time = this.timer.get_last_state_changed_time (); var state = this.session_manager.current_state; var changed = false; if (state != this._state) { changed_properties.add ("{sv}", "State", new GLib.Variant.string (state.to_string ())); changed = true; } if (timer_state.duration != this.timer_state.duration) { changed_properties.add ("{sv}", "Duration", new GLib.Variant.int64 (timer_state.duration)); changed = true; } if (timer_state.offset != this.timer_state.offset) { changed_properties.add ("{sv}", "Offset", new GLib.Variant.int64 (timer_state.offset)); changed = true; } if (timer_state.started_time != this.timer_state.started_time) { changed_properties.add ("{sv}", "StartedTime", new GLib.Variant.int64 (timer_state.started_time)); changed = true; } if (timer_state.paused_time != this.timer_state.paused_time) { changed_properties.add ("{sv}", "PausedTime", new GLib.Variant.int64 (timer_state.paused_time)); changed = true; } if (timer_state.finished_time != this.timer_state.finished_time) { changed_properties.add ("{sv}", "FinishedTime", new GLib.Variant.int64 (timer_state.finished_time)); changed = true; } if (last_state_changed_time != this.last_state_changed_time) { changed_properties.add ("{sv}", "LastChangedTime", new GLib.Variant.int64 (last_state_changed_time)); changed = true; } this._state = state; this.timer_state = timer_state; this.last_state_changed_time = last_state_changed_time; if (changed) { try { this.connection.emit_signal ( null, this.object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", new GLib.Variant ( "(sa{sv}as)", DBUS_INTERFACE_NAME, changed_properties, invalidated_properties ) ); } catch (GLib.Error error) { GLib.warning ("Failed to emit PropertiesChanged signal: %s", error.message); } } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update_properties (); this.changed (); } private void on_timer_tick (int64 timestamp) { this.tick (timestamp); } private void on_timer_finished () { this.finished (); } private void on_session_manager_notify_current_state (GLib.Object object, GLib.ParamSpec pspec) { this.update_properties (); } public bool is_started () throws GLib.DBusError, GLib.IOError { return this.timer.is_started (); } public bool is_running () throws GLib.DBusError, GLib.IOError { return this.timer.is_running (); } public bool is_paused () throws GLib.DBusError, GLib.IOError { return this.timer.is_paused (); } public bool is_finished () throws GLib.DBusError, GLib.IOError { return this.timer.is_finished (); } public int64 get_elapsed (int64 timestamp = Ft.Timestamp.UNDEFINED) throws GLib.DBusError, GLib.IOError { return this.timer.calculate_elapsed (timestamp); } public int64 get_remaining (int64 timestamp = Ft.Timestamp.UNDEFINED) throws GLib.DBusError, GLib.IOError { return this.timer.calculate_remaining (timestamp); } public double get_progress (int64 timestamp = Ft.Timestamp.UNDEFINED) throws GLib.DBusError, GLib.IOError { return this.timer.calculate_progress (timestamp); } public void start () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.start"); this.timer.start (); } public void stop () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.reset"); this.timer.reset (); } public void pause () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.pause"); this.timer.pause (); } public void resume () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.resume"); this.timer.resume (); } public void rewind (int64 interval) throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.rewind"); this.timer.rewind (interval); } public void extend (int64 interval) throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("timer.extend"); this.timer.extend (interval); } public void skip () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("session-manager.advance"); this.session_manager.advance (); } public void reset () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("session-manager.reset"); this.session_manager.reset (); } public signal void changed (); public signal void tick (int64 timestamp); public signal void finished (); public override void dispose () { this.timer.state_changed.disconnect (this.on_timer_state_changed); this.timer.tick.disconnect (this.on_timer_tick); this.timer.finished.disconnect (this.on_timer_finished); this.session_manager.notify["current-state"].disconnect ( this.on_session_manager_notify_current_state); this.timer = null; this.session_manager = null; this.connection = null; base.dispose (); } } /** * Session service represents mostly `SessionManager.current_session`, but * also includes relevant methods/properties from `SessionManager` and scheduler. */ [DBus (name = "io.github.focustimerhq.FocusTimer.Session")] public class SessionDBusService : GLib.Object { private const string DBUS_INTERFACE_NAME = "io.github.focustimerhq.FocusTimer.Session"; public string current_state { owned get { return this._current_state.to_string (); } } public int64 start_time { get { return this._start_time; } } public int64 end_time { get { return this._end_time; } } public bool has_uniform_breaks { get { return this._has_uniform_breaks; } } public bool can_reset { get { return this._can_reset; } } private Ft.SessionManager? session_manager; private weak GLib.DBusConnection? connection; private string object_path; private Ft.State _current_state = Ft.State.STOPPED; private int64 _start_time = Ft.Timestamp.UNDEFINED; private int64 _end_time = Ft.Timestamp.UNDEFINED; private bool _has_uniform_breaks = false; private bool _can_reset = false; private uint changed_idle_id = 0U; public SessionDBusService (GLib.DBusConnection connection, string object_path, Ft.SessionManager session_manager) { this.connection = connection; this.object_path = object_path; this.session_manager = session_manager; this.session_manager.notify["current-session"].connect ( this.on_notify_current_session); this.session_manager.notify["has-uniform-breaks"].connect ( this.on_notify_has_uniform_breaks); this.session_manager.enter_session.connect (this.on_enter_session); this.session_manager.leave_session.connect (this.on_leave_session); this.session_manager.enter_time_block.connect (this.on_enter_time_block); this.session_manager.leave_time_block.connect (this.on_leave_time_block); this.session_manager.confirm_advancement.connect (this.on_confirm_advancement); if (session_manager.current_session != null) { this.on_enter_session (session_manager.current_session); } } private void update_properties () { if (this.connection == null) { return; } var changed_properties = new GLib.VariantBuilder (GLib.VariantType.VARDICT); var invalidated_properties = new GLib.VariantBuilder (new GLib.VariantType ("as")); var current_session = this.session_manager.current_session; var current_state = this.session_manager.current_state; var start_time = current_session != null ? current_session.start_time : Ft.Timestamp.UNDEFINED; var end_time = current_session != null ? current_session.end_time : Ft.Timestamp.UNDEFINED; var has_uniform_breaks = this.session_manager.has_uniform_breaks; var can_reset = this.session_manager.can_reset (); var changed = false; if (this._current_state != current_state) { this._current_state = current_state; changed_properties.add ("{sv}", "CurrentState", new GLib.Variant.string (current_state.to_string ())); changed = true; } if (this._start_time != start_time) { this._start_time = start_time; changed_properties.add ("{sv}", "StartTime", new GLib.Variant.int64 (start_time)); changed = true; } if (this._end_time != end_time) { this._end_time = end_time; changed_properties.add ("{sv}", "EndTime", new GLib.Variant.int64 (end_time)); changed = true; } if (this._has_uniform_breaks != has_uniform_breaks) { this._has_uniform_breaks = has_uniform_breaks; changed_properties.add ("{sv}", "HasUniformBreaks", new GLib.Variant.boolean (has_uniform_breaks)); changed = true; } if (this._can_reset != can_reset) { this._can_reset = can_reset; changed_properties.add ("{sv}", "CanReset", new GLib.Variant.boolean (can_reset)); changed = true; } if (changed) { try { this.connection.emit_signal ( null, this.object_path, "org.freedesktop.DBus.Properties", "PropertiesChanged", new GLib.Variant ( "(sa{sv}as)", DBUS_INTERFACE_NAME, changed_properties, invalidated_properties ) ); } catch (GLib.Error error) { GLib.warning ("Failed to emit PropertiesChanged signal: %s", error.message); } } } private GLib.Variant serialize_gap (Ft.Gap? gap) { var builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT); if (gap != null) { builder.add ("{sv}", "start_time", new GLib.Variant.int64 (gap.start_time)); builder.add ("{sv}", "end_time", new GLib.Variant.int64 (gap.end_time)); } return builder.end (); } private GLib.Variant serialize_time_block (Ft.TimeBlock? time_block) { var builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT); if (time_block != null) { var gaps = new GLib.Variant[0]; time_block.foreach_gap ( (gap) => { gaps += this.serialize_gap (gap); }); builder.add ("{sv}", "state", new GLib.Variant.string (time_block.state.to_string ())); builder.add ("{sv}", "status", new GLib.Variant.string (time_block.get_status ().to_string ())); builder.add ("{sv}", "start_time", new GLib.Variant.int64 (time_block.start_time)); builder.add ("{sv}", "end_time", new GLib.Variant.int64 (time_block.end_time)); builder.add ("{sv}", "gaps", new GLib.Variant.array (GLib.VariantType.VARDICT, gaps)); } return builder.end (); } private GLib.Variant serialize_cycle (Ft.Cycle? cycle) { var builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT); if (cycle != null) { builder.add ("{sv}", "start_time", new GLib.Variant.int64 (cycle.start_time)); builder.add ("{sv}", "end_time", new GLib.Variant.int64 (cycle.end_time)); builder.add ("{sv}", "completion_time", new GLib.Variant.int64 (cycle.get_completion_time ())); builder.add ("{sv}", "weight", new GLib.Variant.double (cycle.get_weight ())); builder.add ("{sv}", "status", new GLib.Variant.string (cycle.get_status ().to_string ())); } return builder.end (); } private void on_notify_current_session (GLib.Object object, GLib.ParamSpec pspec) { this.update_properties (); } private void on_notify_has_uniform_breaks (GLib.Object object, GLib.ParamSpec pspec) { this.update_properties (); } private void on_enter_session (Ft.Session session) { session.changed.connect_after (this.on_current_session_changed); } private void on_leave_session (Ft.Session session) { session.changed.disconnect (this.on_current_session_changed); } private void on_enter_time_block (Ft.TimeBlock time_block) { this.enter_time_block (this.serialize_time_block (time_block)); } private void on_leave_time_block (Ft.TimeBlock time_block) { this.leave_time_block (this.serialize_time_block (time_block)); } private void on_confirm_advancement (Ft.TimeBlock current_time_block, Ft.TimeBlock next_time_block) { this.confirm_advancement (this.serialize_time_block (current_time_block), this.serialize_time_block (next_time_block)); } private void on_current_session_changed (Ft.Session session) { // XXX: ideally we shouldn't need debouncing here if (this.changed_idle_id == 0) { this.changed_idle_id = GLib.Idle.add (() => { this.changed_idle_id = 0; this.update_properties (); this.changed (); return GLib.Source.REMOVE; }); } } public void advance () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("session-manager.advance"); this.session_manager.advance (); } public void advance_to_state (string state) throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source (@"session-manager.state:$(state)"); this.session_manager.advance_to_state (Ft.State.from_string (state)); } public void reset () throws GLib.DBusError, GLib.IOError { Ft.Context.set_event_source ("session-manager.reset"); this.session_manager.reset (); } [DBus (signature = "a{sv}")] public GLib.Variant get_current_time_block () throws GLib.DBusError, GLib.IOError { return this.serialize_time_block (this.session_manager.current_time_block); } [DBus (signature = "a{sv}")] public GLib.Variant get_current_gap () throws GLib.DBusError, GLib.IOError { return this.serialize_gap (this.session_manager.current_gap); } [DBus (signature = "a{sv}")] public GLib.Variant GetNextTimeBlock () throws GLib.DBusError, GLib.IOError { return this.serialize_time_block (this.session_manager.get_next_time_block ()); } [DBus (signature = "aa{sv}")] public GLib.Variant list_time_blocks () throws GLib.DBusError, GLib.IOError { var items = new GLib.Variant[0]; this.session_manager.current_session?.@foreach ( (time_block) => { items += this.serialize_time_block (time_block); }); return new GLib.Variant.array (GLib.VariantType.VARDICT, items); } [DBus (signature = "aa{sv}")] public GLib.Variant list_cycles () throws GLib.DBusError, GLib.IOError { var items = new GLib.Variant[0]; this.session_manager.current_session?.get_cycles ().@foreach ( (cycle) => { items += this.serialize_cycle (cycle); }); return new GLib.Variant.array (GLib.VariantType.VARDICT, items); } public signal void enter_time_block (GLib.Variant time_block); public signal void leave_time_block (GLib.Variant time_block); public signal void confirm_advancement (GLib.Variant current_time_block, GLib.Variant next_time_block); public signal void changed (); public override void dispose () { this.session_manager.notify["current-session"].disconnect ( this.on_notify_has_uniform_breaks); this.session_manager.notify["has-uniform-breaks"].disconnect ( this.on_notify_has_uniform_breaks); this.session_manager.enter_session.disconnect (this.on_enter_session); this.session_manager.leave_session.disconnect (this.on_leave_session); this.session_manager.confirm_advancement.disconnect (this.on_confirm_advancement); if (this.changed_idle_id != 0U) { GLib.Source.remove (this.changed_idle_id); this.changed_idle_id = 0U; } this.session_manager = null; this.connection = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/icons/000077500000000000000000000000001520625676500205615ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/16x16/000077500000000000000000000000001520625676500213465ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/000077500000000000000000000000001520625676500230065ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-pause-symbolic.svg000066400000000000000000000005721520625676500276050ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-reset-symbolic.svg000066400000000000000000000011671520625676500276130ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-rewind-symbolic.svg000066400000000000000000000014021520625676500277510ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-skip-symbolic-rtl.svg000066400000000000000000000010531520625676500302300ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-skip-symbolic.svg000066400000000000000000000010561520625676500274340ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-start-symbolic.svg000066400000000000000000000007001520625676500276160ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16/actions/timer-stop-symbolic.svg000066400000000000000000000004401520625676500274470ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/16x16@2/000077500000000000000000000000001520625676500215305ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/16x16@2/actions/000077500000000000000000000000001520625676500231705ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/16x16@2/actions/timer-rewind-symbolic.svg000066400000000000000000000031061520625676500301360ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/000077500000000000000000000000001520625676500213445ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/000077500000000000000000000000001520625676500230045ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-pause-symbolic.svg000066400000000000000000000004171520625676500276010ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-reset-symbolic.svg000066400000000000000000000012611520625676500276040ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-rewind-symbolic.svg000066400000000000000000000030341520625676500277520ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-skip-symbolic-rtl.svg000066400000000000000000000010001520625676500302160ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-skip-symbolic.svg000066400000000000000000000010001520625676500274170ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-start-symbolic.svg000066400000000000000000000007221520625676500276200ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/24x24/actions/timer-stop-symbolic.svg000066400000000000000000000003061520625676500274460ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/000077500000000000000000000000001520625676500223275ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/000077500000000000000000000000001520625676500237675ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/condition-enter-symbolic.svg000066400000000000000000000010351520625676500314270ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/condition-exit-symbolic.svg000066400000000000000000000010441520625676500312630ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/custom-action-symbolic.svg000066400000000000000000000024651520625676500311230ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/dark-theme-symbolic.svg000066400000000000000000000004721520625676500303530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/document-edit-symbolic.svg000066400000000000000000000007201520625676500310670ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/event-symbolic.svg000066400000000000000000000010121520625676500274420ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/filter-symbolic.svg000066400000000000000000000012671520625676500276220ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/go-last-symbolic.svg000066400000000000000000000012441520625676500276760ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/go-next-symbolic.svg000066400000000000000000000010031520625676500277020ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/go-previous-symbolic.svg000066400000000000000000000010041520625676500306010ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/go-up-symbolic.svg000066400000000000000000000007361520625676500273640ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/list-drag-handle-symbolic.svg000066400000000000000000000014371520625676500314530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/lock-screen-symbolic.svg000066400000000000000000000005511520625676500305350ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/pan-end-custom-symbolic.svg000066400000000000000000000007041520625676500311620ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/pan-start-custom-symbolic.svg000066400000000000000000000006621520625676500315540ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/plugin-symbolic.svg000066400000000000000000000010131520625676500276200ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/preferences-appearance-symbolic.svg000066400000000000000000000025641520625676500327340ustar00rootroot00000000000000 preferences-keyboard-shortcuts-symbolic.svg000066400000000000000000000030401520625676500344000ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/preferences-notifications-symbolic.svg000066400000000000000000000013231520625676500334760ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/preferences-sounds-symbolic.svg000066400000000000000000000023531520625676500321440ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/screen-overlay-close-symbolic.svg000066400000000000000000000010501520625676500323640ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/screen-overlay-open-symbolic.svg000066400000000000000000000033211520625676500322230ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/stats-symbolic.svg000066400000000000000000000006721520625676500274720ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/take-break-symbolic.svg000066400000000000000000000034501520625676500303370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/timer-symbolic.svg000066400000000000000000000017031520625676500274500ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/variable-symbolic.svg000066400000000000000000000025661520625676500301250ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/window-compact-size-symbolic.svg000066400000000000000000000033211520625676500322310ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/icons/scalable/actions/window-normal-size-symbolic.svg000066400000000000000000000010751520625676500320770ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/io.github.focustimerhq.FocusTimer.gresource.xml000066400000000000000000000213531520625676500306500ustar00rootroot00000000000000 migrations/version-1.sql migrations/version-2.sql migrations/version-3.sql ui/log/log-window.ui ui/main/stats/charts/bubble-chart.ui ui/main/stats/charts/chart.ui ui/main/stats/stats-day-page.ui ui/main/stats/stats-month-page.ui ui/main/stats/stats-view.ui ui/main/stats/stats-week-page.ui ui/main/stats/widgets/day-chooser.ui ui/main/stats/widgets/month-chooser.ui ui/main/stats/widgets/stats-card.ui ui/main/stats/widgets/stats-date-popover.ui ui/main/stats/widgets/week-chooser.ui ui/main/timer/widgets/timer-control-buttons.ui ui/main/timer/widgets/timer-label.ui ui/main/timer/compact-timer-view.ui ui/main/timer/menus.ui ui/main/timer/timer-view.ui ui/main/window.ui ui/overlays/lightbox.ui ui/overlays/screen-overlay.ui ui/preferences/appearance/preferences-panel-appearance.ui ui/preferences/automation/action/action-edit-window.ui ui/preferences/automation/action/action-listboxrow.ui ui/preferences/automation/action/command-entryrow.ui ui/preferences/automation/action/condition-group-widget.ui ui/preferences/automation/action/condition-widget.ui ui/preferences/automation/action/variable-popover.ui ui/preferences/automation/preferences-panel-automation.ui ui/preferences/integrations/preferences-panel-integrations.ui ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui ui/preferences/keyboard-shortcuts/accelerator-row.ui ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui ui/preferences/notifications/preferences-panel-notifications.ui ui/preferences/preferences-window.ui ui/preferences/sounds/preferences-panel-sounds.ui ui/preferences/sounds/sound-chooser-window.ui ui/preferences/sounds/volume-slider.ui ui/preferences/timer/widgets/log-scale-row.ui ui/preferences/timer/preferences-panel-timer.ui ui/widgets/sidebar-row.ui ui/preferences/keyboard-shortcuts/enter-keyboard-shortcut.svg ui/style.css icons/16x16/actions/timer-pause-symbolic.svg icons/16x16/actions/timer-reset-symbolic.svg icons/16x16/actions/timer-rewind-symbolic.svg icons/16x16/actions/timer-skip-symbolic.svg icons/16x16/actions/timer-skip-symbolic-rtl.svg icons/16x16/actions/timer-start-symbolic.svg icons/16x16/actions/timer-stop-symbolic.svg icons/16x16@2/actions/timer-rewind-symbolic.svg icons/24x24/actions/timer-pause-symbolic.svg icons/24x24/actions/timer-reset-symbolic.svg icons/24x24/actions/timer-rewind-symbolic.svg icons/24x24/actions/timer-skip-symbolic.svg icons/24x24/actions/timer-skip-symbolic-rtl.svg icons/24x24/actions/timer-start-symbolic.svg icons/24x24/actions/timer-stop-symbolic.svg icons/scalable/actions/condition-enter-symbolic.svg icons/scalable/actions/condition-exit-symbolic.svg icons/scalable/actions/custom-action-symbolic.svg icons/scalable/actions/dark-theme-symbolic.svg icons/scalable/actions/document-edit-symbolic.svg icons/scalable/actions/event-symbolic.svg icons/scalable/actions/filter-symbolic.svg icons/scalable/actions/go-last-symbolic.svg icons/scalable/actions/go-next-symbolic.svg icons/scalable/actions/go-previous-symbolic.svg icons/scalable/actions/go-up-symbolic.svg icons/scalable/actions/list-drag-handle-symbolic.svg icons/scalable/actions/lock-screen-symbolic.svg icons/scalable/actions/pan-end-custom-symbolic.svg icons/scalable/actions/pan-start-custom-symbolic.svg icons/scalable/actions/plugin-symbolic.svg icons/scalable/actions/preferences-appearance-symbolic.svg icons/scalable/actions/preferences-keyboard-shortcuts-symbolic.svg icons/scalable/actions/preferences-notifications-symbolic.svg icons/scalable/actions/preferences-sounds-symbolic.svg icons/scalable/actions/screen-overlay-close-symbolic.svg icons/scalable/actions/screen-overlay-open-symbolic.svg icons/scalable/actions/stats-symbolic.svg icons/scalable/actions/take-break-symbolic.svg icons/scalable/actions/timer-symbolic.svg icons/scalable/actions/variable-symbolic.svg icons/scalable/actions/window-compact-size-symbolic.svg icons/scalable/actions/window-normal-size-symbolic.svg focustimerhq-FocusTimer-8581be2/src/main.vala000066400000000000000000000022001520625676500212310ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; private void on_posix_signal (int signal) { switch (signal) { case Posix.Signal.INT: case Posix.Signal.TERM: var application = Ft.Application.get_default (); if (application != null) { application.quit (); } break; default: break; } } public int main (string[] args) { GLib.Intl.setlocale (GLib.LocaleCategory.ALL, ""); GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.PACKAGE_LOCALE_DIR); GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8"); GLib.Intl.textdomain (Config.GETTEXT_PACKAGE); // translators: Consider "Concentration Timer" as an alternative. GLib.Environment.set_application_name (_("Focus Timer")); GLib.Environment.set_prgname (Config.APPLICATION_ID); Posix.signal (Posix.Signal.INT, on_posix_signal); Posix.signal (Posix.Signal.TERM, on_posix_signal); var application = new Ft.Application (); return application.run (args); } focustimerhq-FocusTimer-8581be2/src/meson.build000066400000000000000000000074711520625676500216210ustar00rootroot00000000000000# Resources resources_sources = files( 'io.github.focustimerhq.FocusTimer.gresource.xml', ) resources = gnome.compile_resources( 'io.github.focustimerhq.FocusTimer', resources_sources, c_name: 'resources', ) # Core subdir('core') # UI library libft_ui_sources = files( 'ui/log/widgets/time-label.vala', 'ui/log/log-window.vala', 'ui/main/dialogs/about-dialog.vala', 'ui/main/stats/charts/bar-chart.vala', 'ui/main/stats/charts/bubble-chart.vala', 'ui/main/stats/charts/canvas-layout.vala', 'ui/main/stats/charts/canvas.vala', 'ui/main/stats/charts/chart-axis.vala', 'ui/main/stats/charts/chart-contents.vala', 'ui/main/stats/charts/chart-grid.vala', 'ui/main/stats/charts/chart.vala', 'ui/main/stats/widgets/day-chooser.vala', 'ui/main/stats/widgets/month-chooser.vala', 'ui/main/stats/widgets/stats-card.vala', 'ui/main/stats/widgets/stats-date-popover.vala', 'ui/main/stats/widgets/week-chooser.vala', 'ui/main/stats/stats-day-page.vala', 'ui/main/stats/stats-month-page.vala', 'ui/main/stats/stats-page.vala', 'ui/main/stats/stats-view.vala', 'ui/main/stats/stats-week-page.vala', 'ui/main/timer/widgets/session-progress-bar.vala', 'ui/main/timer/widgets/timer-control-buttons.vala', 'ui/main/timer/widgets/timer-label.vala', 'ui/main/timer/widgets/timer-progress-bar.vala', 'ui/main/timer/compact-timer-view.vala', 'ui/main/timer/timer-view.vala', 'ui/main/widgets/size-stack.vala', 'ui/main/window.vala', 'ui/overlays/lightbox.vala', 'ui/overlays/screen-overlay.vala', 'ui/preferences/appearance/preferences-panel-appearance.vala', 'ui/preferences/automation/action/action-edit-window.vala', 'ui/preferences/automation/action/action-listboxrow.vala', 'ui/preferences/automation/action/command-entryrow.vala', 'ui/preferences/automation/action/condition-group-widget.vala', 'ui/preferences/automation/action/condition-widget.vala', 'ui/preferences/automation/action/variable-popover.vala', 'ui/preferences/automation/preferences-panel-automation.vala', 'ui/preferences/integrations/preferences-panel-integrations.vala', 'ui/preferences/keyboard-shortcuts/accelerator-chooser-window.vala', 'ui/preferences/keyboard-shortcuts/accelerator-row.vala', 'ui/preferences/keyboard-shortcuts/accelerator.vala', 'ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.vala', 'ui/preferences/notifications/preferences-panel-notifications.vala', 'ui/preferences/sounds/preferences-panel-sounds.vala', 'ui/preferences/sounds/sound-chooser-window.vala', 'ui/preferences/sounds/volume-slider.vala', 'ui/preferences/timer/widgets/log-scale-row.vala', 'ui/preferences/timer/widgets/log-scale.vala', 'ui/preferences/timer/preferences-panel-timer.vala', 'ui/preferences/widgets/preferences-sidebar.vala', 'ui/preferences/preferences-window.vala', 'ui/widgets/checkmark.vala', 'ui/widgets/gizmo.vala', 'ui/widgets/monospace-label.vala', 'ui/widgets/sidebar-row.vala', 'ui/interfaces.vala', 'ui/screen-overlay-provider.vala', 'ui/screen-saver-provider.vala', 'ui/utils.vala', 'application.vala', 'dbus-services.vala', ) libft_ui = static_library( 'ft_ui', libft_ui_sources + resources, dependencies: [ libft_core_dep, gtk_dep, gtk_x11_dep, gtk_wayland_dep, cairo_dep, graphene_dep, libadwaita_dep, peas_dep, ], include_directories: config_h_dir, ) libft_ui_dep = declare_dependency( link_with: libft_ui, dependencies: [ libft_core_dep, gtk_dep, libadwaita_dep, peas_dep, ], include_directories: [ include_directories('.'), ], ) # Plugins subdir('plugins') # The focus-timer executable executable( 'focus-timer', 'main.vala', dependencies: [libft_ui_dep], link_whole: [libft_ui] + libft_plugins, export_dynamic: true, include_directories: config_h_dir, install: true, ) focustimerhq-FocusTimer-8581be2/src/migrations/000077500000000000000000000000001520625676500216225ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/migrations/version-1.sql000066400000000000000000000063321520625676500241720ustar00rootroot00000000000000CREATE TABLE "entries" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "datetime-string" TEXT NOT NULL, -- in UTC "datetime-local-string" TEXT NOT NULL, -- local "state-name" TEXT NOT NULL, "state-duration" INTEGER DEFAULT 0, "elapsed" INTEGER DEFAULT 0 ); CREATE TABLE "aggregated-entries" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "date-string" TEXT NOT NULL, -- local "state-name" TEXT NOT NULL, "state-duration" INTEGER DEFAULT 0, "elapsed" INTEGER DEFAULT 0 ); CREATE INDEX "entries-datetime-local-string" ON "entries" ( "datetime-local-string" ); CREATE INDEX "aggregated-entries-date-string" ON "aggregated-entries" ( "date-string" ); CREATE UNIQUE INDEX "aggregated-entries-date-string-state-name" ON "aggregated-entries" ( "date-string", "state-name" ); CREATE TRIGGER "entries-insert" AFTER INSERT ON "entries" FOR EACH ROW BEGIN UPDATE "aggregated-entries" SET "elapsed" = "elapsed" + NEW."elapsed", "state-duration" = "state-duration" + NEW."state-duration" WHERE "date-string" = date(NEW."datetime-local-string") AND "state-name" = NEW."state-name"; INSERT INTO "aggregated-entries" ( "date-string", "state-name", "state-duration", "elapsed" ) SELECT date(NEW."datetime-local-string"), NEW."state-name", NEW."state-duration", NEW."elapsed" WHERE changes() = 0; END; CREATE TRIGGER "entries-update" AFTER UPDATE ON "entries" FOR EACH ROW BEGIN UPDATE "aggregated-entries" SET "elapsed" = "elapsed" + NEW."elapsed", "state-duration" = "state-duration" + NEW."state-duration" WHERE "date-string" = date(NEW."datetime-local-string") AND "state-name" = NEW."state-name"; INSERT INTO "aggregated-entries" ( "date-string", "state-name", "state-duration", "elapsed" ) SELECT date(NEW."datetime-local-string"), NEW."state-name", NEW."state-duration", NEW."elapsed" WHERE changes() = 0; UPDATE "aggregated-entries" SET "elapsed" = "elapsed" - OLD."elapsed", "state-duration" = "state-duration" - OLD."state-duration" WHERE "date-string" = date(OLD."datetime-local-string") AND "state-name" = OLD."state-name"; END; CREATE TRIGGER "entries-delete" AFTER DELETE ON "entries" FOR EACH ROW BEGIN UPDATE "aggregated-entries" SET "elapsed" = "elapsed" - OLD."elapsed", "state-duration" = "state-duration" - OLD."state-duration" WHERE "date-string" = date(OLD."datetime-local-string") AND "state-name" = OLD."state-name"; END; focustimerhq-FocusTimer-8581be2/src/migrations/version-2.sql000066400000000000000000000133331520625676500241720ustar00rootroot00000000000000CREATE TABLE "sessions" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "start-time" INTEGER NOT NULL UNIQUE, -- Unix timestamp in microseconds, UTC "end-time" INTEGER NOT NULL, -- Unix timestamp in microseconds, UTC "expiry-time" INTEGER NOT NULL, -- Unix timestamp in microseconds, UTC CHECK ("end-time" >= "start-time" OR "end-time" < 0) ); CREATE INDEX "sessions-start-time" ON "sessions" ("start-time"); CREATE TABLE "timeblocks" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "session-id" INTEGER NOT NULL, "start-time" INTEGER NOT NULL UNIQUE, -- Unix timestamp in microseconds, UTC "end-time" INTEGER NOT NULL, -- Unix timestamp in microseconds, UTC "state" TEXT NOT NULL, "status" TEXT NOT NULL, "intended-duration" INTEGER NOT NULL, -- in microseconds FOREIGN KEY ("session-id") REFERENCES "sessions" ("id") ON DELETE CASCADE, CHECK ("end-time" >= "start-time" OR "end-time" < 0) ); CREATE INDEX "time-blocks-session-id" ON "timeblocks" ("session-id"); CREATE TABLE "gaps" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "start-time" INTEGER NOT NULL UNIQUE, -- Unix timestamp in microseconds, UTC "end-time" INTEGER NOT NULL, -- Unix timestamp in microseconds, UTC "flags" TEXT NOT NULL, "time-block-id" INTEGER NOT NULL, -- parent FOREIGN KEY ("time-block-id") REFERENCES "timeblocks" ("id") ON DELETE CASCADE, CHECK ("end-time" >= "start-time" OR "end-time" < 0) ); CREATE INDEX "gaps-time-block-id" ON "gaps" ("time-block-id"); CREATE TABLE "timezones" ( -- Extra `id` column is needed for Gom. We could just use `time` as the -- primary key, but Gom would override given values. "id" INTEGER PRIMARY KEY AUTOINCREMENT, "time" INTEGER NOT NULL UNIQUE, -- Unix timestamp in microseconds, UTC "identifier" TEXT NOT NULL ); CREATE TABLE "stats" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "time" INTEGER NOT NULL, -- Unix timestamp in microseconds, UTC "date" DATE NOT NULL, -- date adjusted for virtual midnight, local "offset" INTEGER NOT NULL, -- time of day in microseconds, local "duration" INTEGER DEFAULT 0, -- in microseconds "category" TEXT NOT NULL, "source-id" INTEGER DEFAULT 0 ); CREATE INDEX "stats-time" ON "stats" ("time"); CREATE INDEX "stats-date" ON "stats" ("date"); CREATE INDEX "stats-category-source-id" ON "stats" ( "category", "source-id" ) WHERE "source-id" != 0; CREATE TABLE "aggregatedstats" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "date" DATE NOT NULL, -- date adjusted for virtual midnight "category" TEXT NOT NULL, "duration" INTEGER DEFAULT 0, -- in microseconds "count" INTEGER DEFAULT 1 ); CREATE INDEX "aggregated-stats-date" ON "aggregatedstats" ("date"); CREATE UNIQUE INDEX "aggregated-stats-date-category" ON "aggregatedstats" ( "date", "category" ); CREATE TRIGGER "stats-insert" AFTER INSERT ON "stats" FOR EACH ROW BEGIN INSERT INTO "aggregatedstats" ( "date", "category", "duration", "count" ) VALUES ( NEW."date", NEW."category", NEW."duration", CASE WHEN NEW."source-id" = 0 THEN 1 WHEN NOT EXISTS ( SELECT 1 FROM "stats" WHERE "category" = NEW."category" AND "source-id" = NEW."source-id" AND "id" <> NEW."id" ) THEN 1 ELSE 0 END ) ON CONFLICT("date","category") DO UPDATE SET "duration" = "duration" + excluded."duration", "count" = "count" + excluded."count"; END; CREATE TRIGGER "stats-update" AFTER UPDATE ON "stats" FOR EACH ROW BEGIN UPDATE "aggregatedstats" SET "duration" = "duration" + NEW."duration" WHERE "date" = NEW."date" AND "category" = NEW."category"; INSERT INTO "aggregatedstats" ( "date", "category", "duration" ) SELECT NEW."date", NEW."category", NEW."duration" WHERE changes() = 0; UPDATE "aggregatedstats" SET "duration" = "duration" - OLD."duration" WHERE "date" = OLD."date" AND "category" = OLD."category"; END; CREATE TRIGGER "stats-delete" AFTER DELETE ON "stats" FOR EACH ROW BEGIN UPDATE "aggregatedstats" SET "duration" = "duration" - OLD."duration", "count" = "count" - CASE WHEN OLD."source-id" = 0 THEN 1 WHEN NOT EXISTS ( SELECT 1 FROM "stats" WHERE "category" = OLD."category" AND "source-id" = OLD."source-id" ) THEN 1 ELSE 0 END WHERE "date" = OLD."date" AND "category" = OLD."category"; END; focustimerhq-FocusTimer-8581be2/src/migrations/version-3.sql000066400000000000000000000050071520625676500241720ustar00rootroot00000000000000-- Populate new "stats" table from legacy "entries" table and drop legacy tables -- Notes: -- - Units conversion: legacy durations are in seconds -> convert to microseconds. -- - Previously we made a single entry for the whole pomodoro / break, now we make an entry for an segment. -- - Legacy "entries" table is dropped. -- - The state-duration column is not used in the new "stats" table. -- - We loose some timezone information. WITH pre AS ( SELECT COALESCE( CAST(strftime('%s', e."datetime-string") AS INTEGER), CAST(strftime('%s', replace(replace(substr(e."datetime-string", 1, 19), 'T', ' '), 'Z', '')) AS INTEGER), CAST(strftime('%s', e."datetime-local-string") AS INTEGER), CAST(strftime('%s', replace(replace(substr(e."datetime-local-string", 1, 19), 'T', ' '), 'Z', '')) AS INTEGER) ) AS "epoch-seconds", COALESCE( replace(replace(substr(e."datetime-local-string", 1, 19), 'T', ' '), 'Z', ''), replace(replace(substr(e."datetime-string", 1, 19), 'T', ' '), 'Z', '') ) AS "local-base", CAST(COALESCE(e."elapsed", 0) AS INTEGER) AS "elapsed-seconds", e."state-name" AS "state-name" FROM "entries" e WHERE e."state-name" IN ('pomodoro', 'break', 'short-break', 'long-break') ), parts AS ( SELECT "epoch-seconds", "elapsed-seconds", "state-name", "local-base", CAST(strftime('%H', "local-base") AS INTEGER) AS "hour", CAST(strftime('%M', "local-base") AS INTEGER) AS "minute", CAST(strftime('%S', "local-base") AS INTEGER) AS "second" FROM pre ) INSERT OR IGNORE INTO "stats" ( "time", "date", "offset", "duration", "category", "source-id" ) SELECT "epoch-seconds" * 1000000 AS "time", CASE WHEN "hour" < 4 THEN date("local-base", '-1 day') ELSE date("local-base") END AS "date", ( "hour" * 3600000000 + "minute" * 60000000 + "second" * 1000000 + CASE WHEN "hour" < 4 THEN 24 * 3600000000 ELSE 0 END ) AS "offset", "elapsed-seconds" * 1000000 AS "duration", CASE "state-name" WHEN 'pomodoro' THEN 'pomodoro' WHEN 'break' THEN 'break' WHEN 'short-break' THEN 'break' WHEN 'long-break' THEN 'break' ELSE NULL END AS "category", 0 AS "source-id" FROM parts; DROP TRIGGER "entries-insert"; DROP TRIGGER "entries-update"; DROP TRIGGER "entries-delete"; DROP TABLE "aggregated-entries"; DROP TABLE "entries"; focustimerhq-FocusTimer-8581be2/src/plugins/000077500000000000000000000000001520625676500211275ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/000077500000000000000000000000001520625676500234425ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/freedesktop.gresource.xml000066400000000000000000000002411520625676500304710ustar00rootroot00000000000000 freedesktop.plugin focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/freedesktop.plugin000066400000000000000000000001661520625676500272000ustar00rootroot00000000000000[Plugin] Name=Freedesktop Module=freedesktop Builtin=true Embedded=freedesktop_peas_register_types X-Priority=default focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/freedesktop.vala000066400000000000000000000017301520625676500266230ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; object_module.register_extension_type (typeof (Ft.NotificationBackendProvider), typeof (Freedesktop.NotificationBackendProvider)); object_module.register_extension_type (typeof (Ft.LockScreenProvider), typeof (Freedesktop.LockScreenProvider)); object_module.register_extension_type (typeof (Ft.SleepMonitorProvider), typeof (Freedesktop.SleepMonitorProvider)); object_module.register_extension_type (typeof (Ft.TimeZoneMonitorProvider), typeof (Freedesktop.TimeZoneMonitorProvider)); } } focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/interfaces.vala000066400000000000000000000074061520625676500264410ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { public struct Session { public string session_id; public uint32 user_id; public string user_name; public string seat_id; public string object_path; } [DBus (name = "org.freedesktop.login1.Manager")] public interface LoginManager : GLib.Object { public abstract async Session[] list_sessions () throws GLib.DBusError, GLib.IOError; public signal void prepare_for_sleep (bool active); } [DBus (name = "org.freedesktop.login1.Session")] public interface LoginSession : GLib.Object { public abstract string id { owned get; } public abstract bool active { get; } public abstract bool locked_hint { get; } [DBus (no_reply = true)] public abstract async void @lock () throws GLib.DBusError, GLib.IOError; } [DBus (name = "org.freedesktop.timedate1")] public interface TimeDate : GLib.Object { public abstract string timezone { owned get; } } public enum NotificationDestroyedReason { EXPIRED = 1, DISMISSED = 2, CLOSED = 3, UNKNOWN = 4; public static NotificationDestroyedReason from_uint (uint32 value) { switch (value) { case EXPIRED: return EXPIRED; case DISMISSED: return DISMISSED; case CLOSED: return CLOSED; default: return UNKNOWN; } } } public enum NotificationUrgency { LOW = 0, NORMAL = 1, CRITICAL = 2; public GLib.Variant to_variant () { return new GLib.Variant.byte ((uint8) this); } } /** * https://specifications.freedesktop.org/notification/latest/protocol.html */ [DBus (name = "org.freedesktop.Notifications")] public interface Notifications : GLib.Object { public abstract async string[] get_capabilities () throws GLib.DBusError, GLib.IOError; public abstract async void get_server_information (out string name, out string vendor, out string version, out string spec_version) throws GLib.DBusError, GLib.IOError; public abstract async uint32 notify (string app_name, uint32 replaces_id, string app_icon, string summary, string body, string[] actions, GLib.HashTable hints, int32 expire_timeout, // milliseconds GLib.Cancellable? cancellable = null) throws GLib.DBusError, GLib.IOError; public abstract async void close_notification (uint32 id) throws GLib.DBusError, GLib.IOError; public signal void action_invoked (uint32 id, string action_key); public signal void activation_token (uint32 id, string activation_token); public signal void notification_closed (uint32 id, uint32 reason); } } focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/lock-screen-provider.vala000066400000000000000000000134571520625676500303560ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { public class LockScreenProvider : Ft.Provider, Ft.LockScreenProvider { public bool active { get { return this._active; } } private Freedesktop.LoginSession? session_proxy = null; private bool _active = false; private uint watcher_id = 0; private ulong properties_changed_id = 0; private void update_active () { var active = this.session_proxy != null ? !this.session_proxy.active || this.session_proxy.locked_hint : false; if (this._active != active) { this._active = active; this.notify_property ("active"); } } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_properties_changed (GLib.Variant changed_properties, string[] invalidated_properties) { this.update_active (); } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SYSTEM, "org.freedesktop.login1", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { try { // /org/freedesktop/login1/session/auto do not send notification when properties change, // for that we need to connect to the exact session object. var manager_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1", GLib.DBusProxyFlags.DO_NOT_AUTO_START, cancellable); var session_auto_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1/session/auto", GLib.DBusProxyFlags.DO_NOT_AUTO_START | GLib.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, cancellable); var login_sessions = yield manager_proxy.list_sessions (); foreach (var login_session in login_sessions) { if (login_session.session_id == session_auto_proxy.id) { this.session_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, "org.freedesktop.login1", login_session.object_path, GLib.DBusProxyFlags.DO_NOT_AUTO_START | GLib.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, cancellable); break; } } if (this.session_proxy == null) { GLib.warning ("Can't connect to current login session. Lock-screen detection will not work."); this.session_proxy = session_auto_proxy; } var session_dbus_proxy = (GLib.DBusProxy) this.session_proxy; this.properties_changed_id = session_dbus_proxy.g_properties_changed.connect ( this.on_properties_changed); this.update_active (); } catch (GLib.Error error) { GLib.warning ("Error while initializing session proxy: %s", error.message); } } public override async void disable () throws GLib.Error { if (this.properties_changed_id != 0) { this.session_proxy.disconnect (this.properties_changed_id); this.properties_changed_id = 0; } this.session_proxy = null; this.update_active (); } public void activate () { if (this.session_proxy != null) { this.session_proxy.@lock.begin ( (obj, res) => { try { this.session_proxy.@lock.end (res); } catch (GLib.Error error) { GLib.warning ("Error while locking the screen: %s", error.message); } }); } else { GLib.warning ("Unable to activate lock-screen."); } } } } focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/meson.build000066400000000000000000000011321520625676500256010ustar00rootroot00000000000000freedesktop_plugin_resources = gnome.compile_resources( 'freedesktop-plugin-resources', 'freedesktop.gresource.xml', c_name: 'freedesktop_plugin', ) libft_plugin_freedesktop = static_library( 'ft_plugin_freedesktop', files( 'interfaces.vala', 'freedesktop.vala', 'lock-screen-provider.vala', 'notification-backend-provider.vala', 'sleep-monitor-provider.vala', 'timezone-monitor-provider.vala', ) + freedesktop_plugin_resources, dependencies: [libft_core_dep, gio_dep, peas_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_freedesktop]focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/notification-backend-provider.vala000066400000000000000000000430011520625676500322100ustar00rootroot00000000000000/* * Copyright (c) 2024-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { /** * Whether clicking the notification activates the app. */ private bool have_default_action (string server_name) { switch (server_name) { case "Xfce Notify Daemon": return false; default: return true; } } /** * Some servers do not read icon name from the .desktop file. */ private string get_application_icon (string server_name) { switch (server_name) { case "cinnamon": case "cosmic-notifications": return @"$(Config.APPLICATION_ID)-symbolic"; case "Xfce Notify Daemon": case "lxqt-notificationd": return Config.APPLICATION_ID; default: return ""; } } private Freedesktop.NotificationUrgency priority_to_urgency (Ft.NotificationPriority priority) { switch (priority) { case Ft.NotificationPriority.LOW: return Freedesktop.NotificationUrgency.LOW; case Ft.NotificationPriority.NORMAL: case Ft.NotificationPriority.HIGH: return Freedesktop.NotificationUrgency.NORMAL; case Ft.NotificationPriority.URGENT: return Freedesktop.NotificationUrgency.CRITICAL; default: assert_not_reached (); } } [Compact] private class NotificationInfo { public Ft.Notification notification; public string id; public uint32 external_id = 0U; public NotificationInfo (string id, Ft.Notification notification) { this.id = id; this.notification = notification; } ~NotificationInfo () { this.notification = null; } } public class NotificationBackendProvider : Ft.Provider, Ft.NotificationBackendProvider { private const int DEFAULT_EXPIRY = -1; private const int NO_EXPIRY = 0; private bool has_actions = false; private bool has_persistence; private bool has_default_action; private uint watcher_id = 0; private Freedesktop.Notifications? proxy = null; private GLib.SList? notifications = null; private GLib.Cancellable? cancellable = null; private GLib.Application? application; private string application_name; private string application_icon; construct { this.application = GLib.Application.get_default (); this.application_name = GLib.Environment.get_application_name (); } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.freedesktop.Notifications", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.cancellable = cancellable != null ? cancellable : new GLib.Cancellable (); try { string name; string vendor; string version; string spec_version; var proxy = yield GLib.Bus.get_proxy ( GLib.BusType.SESSION, "org.freedesktop.Notifications", "/org/freedesktop/Notifications", GLib.DBusProxyFlags.DO_NOT_AUTO_START, this.cancellable); yield proxy.get_server_information (out name, out vendor, out version, out spec_version); var capabilities_strv = yield proxy.get_capabilities (); // TODO: move it to about dialog, troubleshooting section GLib.debug ("Notification backend:\n name: %s\n vendor: %s\n version: %s\n spec_version: %s\n capabilities: %s", name, vendor, version, spec_version, string.joinv (", ",capabilities_strv)); var capabilities = new GLib.GenericSet (GLib.str_hash, GLib.str_equal); foreach (var capability in capabilities_strv) { capabilities.add (capability); } this.has_actions = capabilities.contains ("actions"); this.has_persistence = capabilities.contains ("persistence"); this.has_default_action = have_default_action (name); this.application_icon = get_application_icon (name); this.notifications = new GLib.SList (); this.proxy = proxy; this.proxy.action_invoked.connect (this.on_action_invoked); this.proxy.notification_closed.connect (this.on_notification_closed); } catch (GLib.Error error) { GLib.warning ("Error while creating notifications proxy: %s", error.message); throw error; } } public override async void disable () throws GLib.Error { this.cancellable?.cancel (); unowned var link = this.notifications; while (link != null) { if (link.data.external_id != 0U) { yield this.withdraw_notification_internal (link.data.external_id); } link = link.next; } if (this.proxy != null) { this.proxy.action_invoked.disconnect (this.on_action_invoked); this.proxy.notification_closed.disconnect (this.on_notification_closed); this.proxy = null; } this.notifications = null; this.cancellable = null; } private unowned NotificationInfo? lookup_by_id (string id) { unowned var link = this.notifications; while (link != null) { if (link.data.id == id) { return link.data; } link = link.next; } return null; } private unowned NotificationInfo? lookup_by_external_id (uint32 external_id) { if (external_id == 0U) { return null; } unowned var link = this.notifications; while (link != null) { if (link.data.external_id == external_id) { return link.data; } link = link.next; } return null; } private bool activate_action (string? action, GLib.Variant? parameter) { if (parameter != null && parameter.is_floating ()) { return false; } if (action != null && action.has_prefix ("app.")) { var action_name = action.split (".", 2)[1]; GLib.VariantType? parameter_type = null; var action_group = (GLib.ActionGroup) this.application; if (action_group.query_action (action_name, null, out parameter_type, null, null, null) && ((parameter_type == null && parameter == null) || (parameter_type != null && parameter != null && parameter.is_of_type (parameter_type)))) { action_group.activate_action (action_name, parameter); return true; } } else if (action == null) { this.application.activate (); return true; } return false; } private bool activate_detailed_action (string detailed_action) { string action_name; GLib.Variant? target_value; try { GLib.Action.parse_detailed_name (detailed_action, out action_name, out target_value); return this.activate_action (action_name, target_value); } catch (GLib.Error error) { return false; } } private void on_notification_closed (uint32 id, uint32 reason) { unowned var notification_info = this.lookup_by_external_id (id); if (notification_info == null) { return; } // HACK: Prevent server from putting the notification into history. // It doesn't help if notification hasn't been shown, i.e. in Do Not Disturb mode. if (this.has_persistence && reason == Freedesktop.NotificationDestroyedReason.EXPIRED && notification_info.notification.is_transient) { this.withdraw_notification_internal.begin (notification_info.external_id); } this.notifications.remove (notification_info); } private void on_action_invoked (uint32 id, string action_key) { unowned var notification_info = this.lookup_by_external_id (id); unowned var notification = notification_info?.notification; if (notification == null) { return; } var notification_closed = action_key == "default" ? this.activate_action (notification.default_action, notification.default_target_value) : this.activate_detailed_action (action_key); if (notification_closed) { this.notifications.remove (notification_info); } } private async void withdraw_notification_internal (uint32 external_id) { if (external_id == 0U || this.proxy == null) { return; } try { yield this.proxy.close_notification (external_id); } catch (GLib.Error error) { GLib.warning ("Failed to withdraw notification [%s.%d]: %s", error.domain.to_string (), error.code, error.message); } } private void remove_by_external_id (uint32 external_id) { unowned var link = this.notifications; while (link != null) { if (link.data.external_id == external_id) { unowned var next_link = link.next; this.notifications.remove_link (link); link = next_link; } else { link = link.next; } } } public async void send_notification (string id, Ft.Notification notification) requires (this.proxy != null) { if (this.cancellable == null || this.cancellable.is_cancelled ()) { return; } unowned var existing_notification_info = this.lookup_by_id (id); if (existing_notification_info != null && existing_notification_info.notification.is_similar (notification)) { return; } var replace_id = existing_notification_info != null ? existing_notification_info.external_id : 0U; var actions = new string[0]; if (notification.default_action != null && this.has_default_action) { actions += "default"; actions += ""; } if (this.has_actions) { notification.foreach_button ( (label, action, target_value) => { actions += GLib.Action.print_detailed_name (action, target_value); actions += label; }); } var hints = new GLib.HashTable (GLib.str_hash, GLib.str_equal); hints.insert ("desktop-entry", new GLib.Variant.string (Config.APPLICATION_ID)); hints.insert ("urgency", priority_to_urgency (notification.priority).to_variant ()); if (notification.is_transient) { hints.insert ("transient", new GLib.Variant.boolean (true)); } if (notification.suppress_sound) { hints.insert ("suppress-sound", new GLib.Variant.boolean (true)); } if (notification.category != null) { hints.insert ("category", new GLib.Variant.string (notification.category)); } if (notification.event_id != null) { hints.insert ("event-id", new GLib.Variant.string (notification.event_id)); } var expire_timeout = notification.priority != Ft.NotificationPriority.URGENT ? notification.expire_timeout : NO_EXPIRY; var tmp_notification_info = new Freedesktop.NotificationInfo (id, notification); unowned var notification_info = tmp_notification_info; this.notifications.append ((owned) tmp_notification_info); if (existing_notification_info != null) { replace_id = existing_notification_info.external_id; this.notifications.remove (existing_notification_info); } try { var external_id = yield this.proxy.notify ( this.application_name, replace_id, this.application_icon, notification.title, notification.body ?? "", actions, hints, expire_timeout, this.cancellable); if (replace_id != 0 && replace_id != external_id) { yield this.withdraw_notification_internal (replace_id); } if (this.notifications.index (notification_info) >= 0) { notification_info.external_id = external_id; } else { // Notification got withdrawn while making the call yield this.withdraw_notification_internal (external_id); } } catch (GLib.Error error) { GLib.warning ("Failed to send notification [%s.%d]: %s", error.domain.to_string (), error.code, error.message); // Couldn't replace the notification, at least invalidate the the old one. if (replace_id != 0U) { this.remove_by_external_id (replace_id); yield this.withdraw_notification_internal (replace_id); } } } public async void withdraw_notification (string id) requires (this.proxy != null) { if (this.cancellable == null || this.cancellable.is_cancelled ()) { return; } unowned var notification_info = this.lookup_by_id (id); if (notification_info == null) { return; } var external_id = notification_info != null ? notification_info.external_id : 0U; if (external_id != 0U) { this.remove_by_external_id (external_id); yield this.withdraw_notification_internal (external_id); } else { this.notifications.remove (notification_info); } } public override void dispose () { this.application = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/sleep-monitor-provider.vala000066400000000000000000000050341520625676500307360ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { public class SleepMonitorProvider : Ft.Provider, Ft.SleepMonitorProvider { private Freedesktop.LoginManager? login_manager_proxy = null; private uint watcher_id = 0; private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_prepare_for_sleep (Freedesktop.LoginManager proxy, bool about_to_suspend) { if (about_to_suspend) { this.prepare_for_sleep (); } else { this.woke_up (); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SYSTEM, "org.freedesktop.login1", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.login_manager_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, "org.freedesktop.login1", "/org/freedesktop/login1"); this.login_manager_proxy.prepare_for_sleep.connect (this.on_prepare_for_sleep); } public override async void disable () throws GLib.Error { this.login_manager_proxy.prepare_for_sleep.disconnect (this.on_prepare_for_sleep); this.login_manager_proxy = null; } } } focustimerhq-FocusTimer-8581be2/src/plugins/freedesktop/timezone-monitor-provider.vala000066400000000000000000000067741520625676500314740ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Freedesktop { public class TimeZoneMonitorProvider : Ft.Provider, Ft.TimeZoneMonitorProvider { public string? identifier { get { return this._identifier; } } private Freedesktop.TimeDate? timedate_proxy = null; private uint watcher_id = 0; private ulong properties_changed_id = 0; private string? _identifier = null; private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_properties_changed (GLib.Variant changed_properties, string[] invalidated_properties) { var identifier_value = changed_properties.lookup_value ("Timezone", GLib.VariantType.STRING); if (identifier_value == null) { return; } // `org.freedesktop.timedate1` service can be chatty, so detect no-changes early. var identifier = identifier_value.get_string (); if (this._identifier != identifier) { this._identifier = identifier; this.notify_property ("identifier"); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SYSTEM, "org.freedesktop.timedate1", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.timedate_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SYSTEM, "org.freedesktop.timedate1", "/org/freedesktop/timedate1"); var timedate_dbus_proxy = (GLib.DBusProxy) this.timedate_proxy; this.properties_changed_id = timedate_dbus_proxy.g_properties_changed.connect ( this.on_properties_changed); this._identifier = this.timedate_proxy.timezone; this.notify_property ("identifier"); } public override async void disable () throws GLib.Error { if (this.properties_changed_id != 0) { this.timedate_proxy.disconnect (this.properties_changed_id); this.properties_changed_id = 0; } this.timedate_proxy = null; } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/000077500000000000000000000000001520625676500222345ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/gnome/application-extension.vala000066400000000000000000000114151520625676500274200ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { public class ApplicationExtension : Ft.ApplicationExtension { private Ft.BackgroundManager? background_manager = null; private Ft.NotificationManager? notification_manager = null; private Gnome.Shell? shell_proxy = null; private Gnome.ShellExtension? shell_extension = null; private bool shell_extension_enabled = false; private uint shell_watcher_id = 0; private uint background_hold_id = 0U; private GLib.Cancellable? cancellable = null; construct { this.background_manager = new Ft.BackgroundManager (); this.notification_manager = new Ft.NotificationManager (); this.notification_manager.screen_overlay_opened.connect (this.on_screen_overlay_opened); this.shell_extension = new Gnome.ShellExtension (); this.shell_extension.notify["enabled"].connect (this.on_shell_extension_notify_enabled); this.shell_watcher_id = GLib.Bus.watch_name ( GLib.BusType.SESSION, "org.gnome.Shell", GLib.BusNameWatcherFlags.NONE, this.on_shell_name_appeared, this.on_shell_name_vanished); this.update (); } private void update () { var notification_manager = new Ft.NotificationManager (); var shell_extension_enabled = this.shell_extension.enabled; if (this.shell_extension_enabled == shell_extension_enabled) { return; } this.shell_extension_enabled = shell_extension_enabled; if (shell_extension_enabled) { if (this.background_hold_id == 0U) { this.background_hold_id = this.background_manager.hold_sync (); } notification_manager.inhibit (); } else { if (this.background_hold_id != 0U) { this.background_manager.release (this.background_hold_id); this.background_hold_id = 0U; } notification_manager.uninhibit (); } } private void on_shell_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (this.shell_proxy != null) { return; } try { this.shell_proxy = GLib.Bus.get_proxy_sync ( GLib.BusType.SESSION, "org.gnome.Shell", "/org/gnome/Shell", GLib.DBusProxyFlags.DO_NOT_AUTO_START | GLib.DBusProxyFlags.DO_NOT_CONNECT_SIGNALS, this.cancellable); } catch (GLib.Error error) { GLib.warning ("Error while initializing shell proxy: %s", error.message); } } private void on_shell_name_vanished (GLib.DBusConnection? connection, string name) { this.shell_proxy = null; } private void on_shell_extension_notify_enabled (GLib.Object object, GLib.ParamSpec pspec) { this.update (); } private void on_screen_overlay_opened () { if (this.shell_proxy != null && this.shell_proxy.overview_active) { this.shell_proxy.overview_active = false; } } public override void dispose () { if (this.shell_watcher_id != 0) { GLib.Bus.unwatch_name (this.shell_watcher_id); this.shell_watcher_id = 0; } if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } if (this.background_hold_id != 0U) { this.background_manager.release (this.background_hold_id); this.background_hold_id = 0U; } if (this.notification_manager != null) { this.notification_manager.screen_overlay_opened.disconnect (this.on_screen_overlay_opened); this.notification_manager = null; } this.shell_extension = null; this.shell_proxy = null; this.background_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/gnome.gresource.xml000066400000000000000000000003611520625676500260600ustar00rootroot00000000000000 gnome.plugin install-extension-dialog.ui focustimerhq-FocusTimer-8581be2/src/plugins/gnome/gnome.plugin000066400000000000000000000001411520625676500245550ustar00rootroot00000000000000[Plugin] Name=GNOME Module=gnome Builtin=true Embedded=gnome_peas_register_types X-Priority=high focustimerhq-FocusTimer-8581be2/src/plugins/gnome/gnome.vala000066400000000000000000000026311520625676500242100ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; object_module.register_extension_type (typeof (Ft.ApplicationExtension), typeof (Gnome.ApplicationExtension)); object_module.register_extension_type (typeof (Ft.WindowExtension), typeof (Gnome.WindowExtension)); object_module.register_extension_type (typeof (Ft.PreferencesWindowExtension), typeof (Gnome.PreferencesWindowExtension)); object_module.register_extension_type (typeof (Ft.IdleMonitorProvider), typeof (Gnome.IdleMonitorProvider)); object_module.register_extension_type (typeof (Ft.ScreenSaverProvider), typeof (Gnome.ScreenSaverProvider)); object_module.register_extension_type (typeof (Ft.ScreenOverlayProvider), typeof (Gnome.ScreenOverlayProvider)); object_module.register_extension_type (typeof (Ft.IndicatorProvider), typeof (Gnome.IndicatorProvider)); } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/idle-monitor-provider.vala000066400000000000000000000300141520625676500273310ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { public class IdleMonitorProvider : Ft.Provider, Ft.IdleMonitorProvider { /** * We typically define the timeout relative to a given reference-time. Mutter counts idle-time from the * perspective of a user and refers to idle-time as an interval, since the watch may be triggered repeatedly. * Allow some tolerance in order not to schedule relative timeouts and prefer native intervals. */ private const int64 TIMEOUT_TOLERANCE = 100 * Ft.Interval.MILLISECOND; [Compact] class Watch { public uint32 id = 0; public int64 absolute_timeout = 0; public int64 relative_timeout = 0; public int64 reference_time = Ft.Timestamp.UNDEFINED; public bool has_active_watch = false; public bool invalid = false; } public Ft.Priority priority { get { return Ft.Priority.DEFAULT; } } public bool can_ignore_inhibitors { get { return false; } } private Gnome.IdleMonitor? proxy = null; private GLib.Cancellable? cancellable = null; private uint dbus_watcher_id = 0; private GLib.HashTable watches = null; private uint32 active_watch_id = 0; private uint active_watch_use_count = 0; private int idle_time_freeze_count = 0; private int64 idle_time = -1; construct { this.watches = new GLib.HashTable (int64_hash, int64_equal); } private inline int64 from_milliseconds (uint64 milliseconds) { return milliseconds < int64.MAX / Ft.Interval.MILLISECOND ? (int64) milliseconds * Ft.Interval.MILLISECOND : int64.MAX; } private inline uint64 to_milliseconds (int64 interval) { return (uint64) int64.max (interval, 0) / Ft.Interval.MILLISECOND; } private void freeze_idle_time () { this.idle_time_freeze_count++; } private void thaw_idle_time () { this.idle_time_freeze_count--; } private void remove_active_watch_internal () throws GLib.Error { if (this.active_watch_id != 0) { var watch_id = this.active_watch_id; this.active_watch_id = 0; this.active_watch_use_count = 0; this.proxy.remove_watch (watch_id); } } private int64 get_idle_time () throws GLib.Error { if (this.idle_time_freeze_count > 0 && this.idle_time >= 0) { return this.idle_time; } if (this.proxy == null) { return 0; } var idle_time = this.from_milliseconds (this.proxy.get_idletime ()); if (this.idle_time_freeze_count > 0) { this.idle_time = idle_time; } return idle_time; } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_became_active () { try { this.remove_active_watch_internal (); } catch (GLib.Error error) { GLib.warning ("Error while removing active-watch: %s", error.message); } this.became_active (); } private void on_became_idle (Watch watch) { var monotonic_time = GLib.get_monotonic_time (); var min_elapsed = int64.max (watch.relative_timeout - TIMEOUT_TOLERANCE, watch.relative_timeout / 2); if (monotonic_time - watch.reference_time >= min_elapsed) { this.became_idle (watch.id); } } private void on_watch_fired (Gnome.IdleMonitor idle_monitor, uint32 id) { if (id == 0) { return; } this.freeze_idle_time (); if (id == this.active_watch_id) { this.idle_time = 0; this.on_became_active (); } else { unowned Watch? watch = this.watches.lookup (id); if (watch != null && !watch.invalid) { this.idle_time = watch.absolute_timeout; this.on_became_idle (watch); } } this.thaw_idle_time (); } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { if (this.dbus_watcher_id == 0) { this.dbus_watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.gnome.Mutter.IdleMonitor", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } } public override async void uninitialize () throws GLib.Error { if (this.dbus_watcher_id != 0) { GLib.Bus.unwatch_name (this.dbus_watcher_id); this.dbus_watcher_id = 0; } this.cancellable = null; } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { if (this.proxy != null) { return; } this.cancellable = new GLib.Cancellable (); this.proxy = yield GLib.Bus.get_proxy ( GLib.BusType.SESSION, "org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", GLib.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES, this.cancellable); this.proxy.watch_fired.connect (this.on_watch_fired); } public override async void disable () throws GLib.Error { if (this.cancellable != null) { this.cancellable.cancel (); } if (this.proxy == null) { return; } this.proxy.watch_fired.disconnect (this.on_watch_fired); uint32[] ids = {}; this.watches.@foreach ( (id, watch) => { ids += watch.id; }); for (var index = 0; index < ids.length; index++) { this.remove_idle_watch (ids[index]); } this.remove_active_watch_internal (); this.proxy = null; } public uint32 add_idle_watch (int64 timeout, bool ignore_inhibitors, int64 monotonic_time) throws GLib.Error requires (this.proxy != null) { int64 relative_timeout = timeout; int64 absolute_timeout = timeout; int64 idle_time; if (Ft.Timestamp.is_undefined (monotonic_time)) { monotonic_time = GLib.get_monotonic_time () - relative_timeout; } else { idle_time = this.get_idle_time (); absolute_timeout = calculate_absolute_timeout (relative_timeout, idle_time, monotonic_time); if ((absolute_timeout - relative_timeout).abs () < TIMEOUT_TOLERANCE) { absolute_timeout = relative_timeout; } } var watch_id = this.proxy.add_idle_watch (this.to_milliseconds (absolute_timeout)); var watch = new Watch (); watch.id = watch_id; watch.relative_timeout = relative_timeout; watch.absolute_timeout = absolute_timeout; watch.reference_time = monotonic_time; unowned Watch _watch = watch; this.watches.insert (watch_id, (owned) watch); if (!_watch.has_active_watch && _watch.absolute_timeout != _watch.relative_timeout) { try { this.add_active_watch (); _watch.has_active_watch = true; } catch (GLib.Error error) { GLib.debug ("Unable to add active watch: %s", error.message); this.remove_idle_watch (watch_id); throw error; } } return watch_id; } public void remove_idle_watch (uint32 id) requires (this.proxy != null) { unowned Watch? watch = this.watches.lookup (id); if (watch == null) { return; } watch.invalid = true; /* if (watch.has_active_watch) { // XXX: why do we need this? try { this.remove_active_watch (); watch.has_active_watch = false; } catch (GLib.Error error) { GLib.warning ("Unable to remove active watch: %s", error.message); } } */ try { this.proxy.remove_watch (watch.id); if (!watch.has_active_watch) { this.watches.remove (id); } } catch (GLib.Error error) { GLib.warning ("Error while removing idle watch: %s", error.message); } } public uint32 reset_idle_watch (uint32 id, int64 monotonic_time) throws GLib.Error requires (this.proxy != null) { unowned Watch? watch = this.watches.lookup (id); if (watch == null || watch.absolute_timeout == watch.relative_timeout) { return id; } var new_id = this.add_idle_watch (watch.relative_timeout, false, monotonic_time); try { this.proxy.remove_watch (watch.id); } catch (GLib.Error error) { this.remove_idle_watch (new_id); throw error; } return new_id; } public void add_active_watch () throws GLib.Error requires (this.proxy != null) { if (this.active_watch_id == 0) { this.active_watch_id = this.proxy.add_user_active_watch (); } this.active_watch_use_count++; } public void remove_active_watch () throws GLib.Error requires (this.active_watch_use_count > 0) { if (this.active_watch_use_count > 1) { this.active_watch_use_count--; } else if (this.active_watch_use_count == 1) { this.remove_active_watch_internal (); } } public override void dispose () { this.watches = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/indicator-provider.vala000066400000000000000000000037331520625676500267130ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { /** * Provider for the indicator when the extension is enabled. * * Extension manages the indicator. Just prevent other implementations from getting enabled. */ public class IndicatorProvider : Ft.Provider, Ft.IndicatorProvider { public bool visible { get { return this.shell_extension != null && this.shell_extension.enabled; } } private Gnome.ShellExtension? shell_extension = null; private void update_available () { this.available = this.shell_extension != null && this.shell_extension.enabled; } private void on_notify_extension_enabled (GLib.Object object, GLib.ParamSpec pspec) { this.update_available (); this.notify_property ("visible"); } protected override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.shell_extension = new Gnome.ShellExtension (); this.shell_extension.notify["enabled"].connect (this.on_notify_extension_enabled); this.update_available (); } protected override async void uninitialize () throws GLib.Error { if (this.shell_extension != null) { this.shell_extension.notify["enabled"].disconnect (this.on_notify_extension_enabled); this.shell_extension = null; this.update_available (); } } protected override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { // Indicator is already enabled by the extension. Nothing to do. } protected override async void disable () throws GLib.Error { } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/install-extension-dialog.ui000066400000000000000000000546121520625676500275200ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/gnome/install-extension-dialog.vala000066400000000000000000000112101520625676500300110ustar00rootroot00000000000000/* * Copyright (c) 2025-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Gnome { [GtkTemplate (ui = "/plugins/gnome/install-extension-dialog.ui")] public class InstallExtensionDialog : Adw.Dialog { private uint CLOSE_TIMEOUT_SECONDS = 3; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Label error_message_label; [GtkChild] private unowned Gtk.TextView error_text_view; private Gnome.ShellExtension? shell_extension = null; private uint timeout_id = 0U; construct { this.shell_extension = new Gnome.ShellExtension (); } private void close_after_timeout (uint seconds) { if (this.timeout_id != 0U) { GLib.Source.remove (this.timeout_id); } this.timeout_id = GLib.Timeout.add_seconds ( seconds, () => { this.timeout_id = 0U; this.close (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.timeout_id, "Gnome.InstallExtensionDialog.close_after_timeout"); } private void show_spinner () { this.stack.visible_child_name = "spinner"; this.can_close = false; } [GtkCallback] private void on_install_clicked () { if (!this.can_close) { return; } this.show_spinner (); this.shell_extension.install_extension.begin ( (obj, res) => { try { var success = this.shell_extension.install_extension.end (res); this.can_close = true; if (success) { this.stack.visible_child_name = "success"; this.close_after_timeout (CLOSE_TIMEOUT_SECONDS); } else { this.close (); } } catch (Gnome.ShellExtensionError error) { switch (error.code) { case Gnome.ShellExtensionError.TIMED_OUT: this.error_message_label.label = _("Time-out reached"); this.error_message_label.visible = true; break; case Gnome.ShellExtensionError.NOT_ALLOWED: this.error_message_label.label = _("Installing extensions is not allowed"); this.error_message_label.visible = true; break; case Gnome.ShellExtensionError.DOWNLOAD_FAILED: this.error_message_label.label = _("Failed to download the extension"); this.error_message_label.visible = true; break; default: this.error_text_view.buffer.text = error.message; this.error_message_label.visible = false; break; } this.can_close = true; this.stack.visible_child_name = "failure"; } }); } [GtkCallback] private void on_cancel_clicked () { if (!this.can_close) { return; } this.close (); } [GtkCallback] private void on_abort_clicked () { if (!this.can_close) { return; } this.close (); } [GtkCallback] private void on_copy_to_clipboard_clicked (Gtk.Button button) { var display = Gdk.Display.get_default (); var error_message = this.error_text_view.buffer.text; if (display != null) { var clipboard = display.get_clipboard (); clipboard.set_text (error_message); } } public override void dispose () { if (this.timeout_id != 0U) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0U; } this.shell_extension = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/interfaces.vala000066400000000000000000000175271520625676500252400ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { [DBus (name = "org.gnome.ScreenSaver")] public interface ScreenSaver : GLib.Object { public abstract async bool get_active () throws GLib.DBusError, GLib.IOError; [DBus (no_reply = true)] public abstract async void @lock () throws GLib.DBusError, GLib.IOError; public signal void active_changed (bool active); } [DBus (name = "org.gnome.Mutter.IdleMonitor")] public interface IdleMonitor : GLib.Object { public abstract uint32 add_idle_watch (uint64 interval) throws GLib.DBusError, GLib.IOError; public abstract uint32 add_user_active_watch () throws GLib.DBusError, GLib.IOError; public abstract uint64 get_idletime () throws GLib.DBusError, GLib.IOError; public abstract void remove_watch (uint32 id) throws GLib.DBusError, GLib.IOError; public signal void watch_fired (uint32 id); } [DBus (name = "org.gnome.Shell")] public interface Shell : GLib.Object { public abstract bool overview_active { get; set; } } public enum ExtensionType { UNKNOWN = 0, SYSTEM = 1, PER_USER = 2; public string to_string () { switch (this) { case SYSTEM: return "system"; case PER_USER: return "per-user"; case UNKNOWN: return ""; default: assert_not_reached (); } } } public enum ExtensionState { UNKNOWN = 0, ENABLED = 1, INACTIVE = 2, ERROR = 3, OUT_OF_DATE = 4, DOWNLOADING = 5, INITIALIZED = 6, DEACTIVATING = 7, ACTIVATING = 8, // Used as an error state for operations on unknown extensions UNINSTALLED = 99; public string to_string () { switch (this) { case ENABLED: return "enabled"; case INACTIVE: return "inactive"; case ERROR: return "error"; case OUT_OF_DATE: return "out-of-date"; case DOWNLOADING: return "downloading"; case INITIALIZED: return "initialized"; case UNINSTALLED: return "uninstalled"; case DEACTIVATING: return "deactivating"; case ACTIVATING: return "activating"; case UNKNOWN: return ""; default: assert_not_reached (); } } } public struct ExtensionInfo { public string uuid; public Gnome.ExtensionType type; public Gnome.ExtensionState state; public bool enabled; public string path; public string error; public string settings_schema; public bool has_prefs; public bool has_update; public ExtensionInfo (string uuid) { this.uuid = uuid; this.type = Gnome.ExtensionType.UNKNOWN; this.state = Gnome.ExtensionState.UNKNOWN; this.enabled = false; this.path = ""; this.error = ""; this.settings_schema = ""; } public ExtensionInfo.deserialize (string uuid, GLib.HashTable data) { this.uuid = data.contains ("uuid") ? data.lookup ("uuid").get_string () : uuid; this.type = data.contains ("type") ? (Gnome.ExtensionType) data.lookup ("type").get_double () : Gnome.ExtensionType.UNKNOWN; this.state = data.contains ("state") ? (Gnome.ExtensionState) data.lookup ("state").get_double () : Gnome.ExtensionState.UNINSTALLED; this.enabled = data.contains ("enabled") ? data.lookup ("enabled").get_boolean () : false; this.path = data.contains ("path") ? data.lookup ("path").get_string () : ""; this.error = data.contains ("error") ? data.lookup ("error").get_string () : ""; this.settings_schema = data.contains ("settings-schema") ? data.lookup ("settings-schema").get_string () : ""; this.has_prefs = data.contains ("hasPrefs") ? data.lookup ("hasPrefs").get_boolean () : false; this.has_update = data.contains ("hasUpdate") ? data.lookup ("hasUpdate").get_boolean () : false; } public string to_representation () { var representation = new GLib.StringBuilder ("ExtensionInfo (\n"); representation.append (@" uuid = $uuid,\n"); representation.append (@" type = $(type.to_string()),\n"); representation.append (@" state = $(state.to_string()),\n"); representation.append (@" enabled = $enabled,\n"); representation.append (@" path = $path,\n"); representation.append (@" error = $error,\n"); representation.append (@" settings_schema = $settings_schema,\n"); representation.append (@" has_prefs = $has_prefs,\n"); representation.append (@" has_update = $has_update\n"); representation.append (")"); return representation.str; } } [DBus (name = "org.gnome.Shell.Extensions")] public interface ShellExtensions : GLib.Object { public abstract bool user_extensions_enabled { get; set; } public abstract async bool enable_extension (string uuid) throws GLib.DBusError, GLib.IOError; public abstract async bool disable_extension (string uuid) throws GLib.DBusError, GLib.IOError; public abstract async GLib.HashTable get_extension_info (string uuid) throws GLib.DBusError, GLib.IOError; public abstract async string install_remote_extension (string uuid) throws GLib.DBusError, GLib.IOError; public abstract async bool uninstall_extension (string uuid) throws GLib.DBusError, GLib.IOError; public signal void extension_state_changed (string uuid, GLib.HashTable state); } [DBus (name = "io.github.focustimerhq.FocusTimer.ShellIntegration")] public interface ShellIntegration : GLib.Object { public abstract string version { owned get; } public abstract string indicator_type { owned get; set; } public abstract bool enable_blur_effect { get; set; } public abstract bool enable_dismiss_gesture { get; set; } public abstract bool enable_manage_notifications { get; set; } public abstract async void open_screen_overlay () throws GLib.DBusError, GLib.IOError; } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/meson.build000066400000000000000000000014601520625676500243770ustar00rootroot00000000000000if get_option('plugin_gnome').enabled() gnome_plugin_resources = gnome.compile_resources( 'gnome-plugin-resources', 'gnome.gresource.xml', c_name: 'gnome_plugin', ) libft_plugin_gnome = static_library( 'ft_plugin_gnome', files( 'application-extension.vala', 'gnome.vala', 'idle-monitor-provider.vala', 'indicator-provider.vala', 'install-extension-dialog.vala', 'interfaces.vala', 'preferences-window-extension.vala', 'screen-overlay-provider.vala', 'screen-saver-provider.vala', 'shell-extension.vala', 'shell-extension-settings.vala', 'window-extension.vala', ) + gnome_plugin_resources, dependencies: [libft_ui_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_gnome] endiffocustimerhq-FocusTimer-8581be2/src/plugins/gnome/preferences-window-extension.vala000066400000000000000000000432301520625676500307230ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { public class PreferencesWindowExtension : Ft.PreferencesWindowExtension { private Gnome.ShellExtension? shell_extension = null; private Gnome.ShellExtensionSettings? settings = null; private Ft.PreferencesPanel? last_panel = null; private unowned Adw.PreferencesGroup? indicator_group = null; private unowned Adw.PreferencesGroup? screen_overlay_group = null; private unowned Adw.PreferencesGroup? desktop_group = null; private unowned Gtk.Switch? shell_extension_toggle = null; private unowned Adw.SwitchRow? manage_notifications_row = null; private GLib.Binding? manage_notifications_binding = null; private bool installing_extension = false; construct { this.shell_extension = new Gnome.ShellExtension (); this.shell_extension.notify["settings"].connect (this.on_notify_settings); this.shell_extension.notify["available"].connect (this.on_notify_available); this.notify["window"].connect (this.on_notify_window); this.update_settings (); } private Adw.Toggle create_toggle (string name, string label) { var toggle = new Adw.Toggle (); toggle.name = name; toggle.label = label; return toggle; } private void setup_appearance_panel (Ft.PreferencesPanel panel) { var page = panel.get_preferences_page (); if (this.settings == null) { this.taredown_appearance_panel (panel); return; } if (this.indicator_group == null) { var indicator_group = new Adw.PreferencesGroup (); indicator_group.title = _("Indicator"); page.add (indicator_group); var indicator_type_toggle_group = new Adw.ToggleGroup (); indicator_type_toggle_group.homogeneous = true; indicator_type_toggle_group.can_shrink = false; indicator_type_toggle_group.valign = Gtk.Align.CENTER; indicator_type_toggle_group.add (this.create_toggle ("icon", _("Icon"))); indicator_type_toggle_group.add (this.create_toggle ("text", _("Text"))); this.settings.bind_property ( "indicator-type", indicator_type_toggle_group, "active-name", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); var indicator_type_row = new Adw.ActionRow (); indicator_type_row.title = _("Display As"); indicator_type_row.activatable = false; indicator_type_row.add_suffix (indicator_type_toggle_group); indicator_group.add (indicator_type_row); this.indicator_group = indicator_group; } if (this.screen_overlay_group == null) { var screen_overlay_group = new Adw.PreferencesGroup (); screen_overlay_group.title = _("Screen Overlay"); page.add (screen_overlay_group); var blur_effect_row = new Adw.SwitchRow (); blur_effect_row.title = _("Blur Effect"); this.settings.bind_property ( "blur-effect", blur_effect_row, "active", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); screen_overlay_group.add (blur_effect_row); var dismiss_gesture_row = new Adw.SwitchRow (); dismiss_gesture_row.title = _("Dismiss Gesture"); this.settings.bind_property ( "dismiss-gesture", dismiss_gesture_row, "active", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); screen_overlay_group.add (dismiss_gesture_row); this.screen_overlay_group = screen_overlay_group; } } private void taredown_appearance_panel (Ft.PreferencesPanel panel) { this.indicator_group?.unparent (); this.indicator_group = null; this.screen_overlay_group?.unparent (); this.screen_overlay_group = null; } private void setup_integrations_panel (Ft.PreferencesPanel panel) { var page = panel.get_preferences_page (); if (this.shell_extension == null || !this.shell_extension.available) { this.taredown_integrations_panel (panel); return; } if (this.desktop_group == null) { var desktop_group = new Adw.PreferencesGroup (); desktop_group.title = _("Desktop"); page.add (desktop_group); var install_button = new Gtk.Button.with_label (_("Install")); install_button.add_css_class ("suggested-action"); install_button.clicked.connect (() => this.install_extension ()); /* translators: verb */ var update_button = new Gtk.Button.with_label (_("Update")); update_button.add_css_class ("suggested-action"); update_button.clicked.connect ( () => this.install_extension (_("Log out to finish the update"))); // XXX: not tested var toggle = new Gtk.Switch (); toggle.valign = Gtk.Align.CENTER; toggle.notify["active"].connect ( (object, pspec) => { if (toggle.active == this.shell_extension.enabled) { return; } if (toggle.active) { this.shell_extension.enable_extension.begin (); } else { this.shell_extension.disable_extension.begin (); } }); var outdated_button = new Gtk.Button.with_label (_("Outdated")); outdated_button.sensitive = false; var state_stack = new Gtk.Stack (); state_stack.hhomogeneous = false; state_stack.vhomogeneous = true; state_stack.valign = Gtk.Align.CENTER; state_stack.add_named (install_button, "uninstalled"); state_stack.add_named (toggle, "installed"); state_stack.add_named (outdated_button, "outdated"); state_stack.add_named (update_button, "update"); var extension_row = new Adw.ActionRow (); extension_row.title = _("GNOME Shell Extension"); extension_row.add_suffix (state_stack); extension_row.set_activatable_widget (toggle); desktop_group.add (extension_row); var manage_notifications_row = new Adw.SwitchRow (); manage_notifications_row.title = _("Manage Notifications"); manage_notifications_row.subtitle = _("Toggle Do Not Disturb mode during Pomodoro."); desktop_group.add (manage_notifications_row); state_stack.bind_property ( "visible-child-name", extension_row, "activatable", GLib.BindingFlags.SYNC_CREATE, this.transform_visible_child_name_to_activatable); this.shell_extension.bind_property ( "enabled", toggle, "active", GLib.BindingFlags.SYNC_CREATE); this.shell_extension.bind_property ( "state", state_stack, "visible-child-name", GLib.BindingFlags.SYNC_CREATE, this.transform_state_to_visible_child_name); this.shell_extension.bind_property ( "state", desktop_group, "visible", GLib.BindingFlags.SYNC_CREATE, this.transform_state_to_can_install); this.shell_extension.bind_property ( "state", manage_notifications_row, "visible", GLib.BindingFlags.SYNC_CREATE, this.transform_state_to_is_installed); // TODO: display indicators about extension error or update this.desktop_group = desktop_group; this.shell_extension_toggle = toggle; this.manage_notifications_row = manage_notifications_row; } if (this.shell_extension_toggle != null) { this.shell_extension_toggle.state = this.settings != null; } if (this.manage_notifications_row != null) { this.manage_notifications_row.sensitive = this.settings != null; } if (this.manage_notifications_row != null && this.settings != null) { this.manage_notifications_binding?.unbind (); this.manage_notifications_binding = this.settings.bind_property ( "manage-notifications", this.manage_notifications_row, "active", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); } } private void taredown_integrations_panel (Ft.PreferencesPanel panel) { this.shell_extension_toggle = null; this.manage_notifications_row = null; this.manage_notifications_binding?.unbind (); this.manage_notifications_binding = null; this.desktop_group?.unparent (); this.desktop_group = null; } private bool transform_state_to_can_install (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { target_value.set_boolean (this.shell_extension.is_installed () || Gnome.ShellExtension.IS_PUBLISHED); return true; } private bool transform_state_to_is_installed (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { target_value.set_boolean (this.shell_extension.is_installed ()); return true; } private bool transform_state_to_visible_child_name (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var state = (Gnome.ExtensionState) source_value.get_enum (); switch (state) { case Gnome.ExtensionState.UNINSTALLED: case Gnome.ExtensionState.DOWNLOADING: target_value.set_string ("uninstalled"); break; case Gnome.ExtensionState.ENABLED: case Gnome.ExtensionState.INACTIVE: case Gnome.ExtensionState.DEACTIVATING: case Gnome.ExtensionState.ACTIVATING: case Gnome.ExtensionState.INITIALIZED: case Gnome.ExtensionState.ERROR: target_value.set_string ("installed"); break; case Gnome.ExtensionState.OUT_OF_DATE: // TODO: Not sure how `has_update` works. Does it return `true` when update is // available or has been already downloaded? // if (this.shell_extension.has_update () && Gnome.ShellExtension.IS_PUBLISHED) { // target_value.set_string ("update"); // } // else { target_value.set_string ("outdated"); // } break; default: return false; } return true; } private bool transform_visible_child_name_to_activatable (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { target_value.set_boolean (source_value.get_string () == "installed"); return true; } /** * Modify visible_panel of the PreferencesWindow. * * We allow to rerun setup functions as `settings` may vanish and reappear. */ private void setup () { var panel = this.window?.visible_panel; if (panel != this.last_panel) { switch (this.last_panel?.tag) { case "appearance": this.taredown_appearance_panel (this.last_panel); break; case "integrations": this.taredown_integrations_panel (this.last_panel); break; } this.last_panel = panel; } switch (panel?.tag) { case "appearance": this.setup_appearance_panel (panel); break; case "integrations": this.setup_integrations_panel (panel); break; default: this.taredown (); break; } } private void taredown () { if (this.last_panel == null) { return; } this.taredown_appearance_panel (this.last_panel); this.taredown_integrations_panel (this.last_panel); this.last_panel = null; } private void install_extension (string success_message = "") { var shell_extension = this.shell_extension; if (this.installing_extension) { return; } this.installing_extension = true; shell_extension.install_extension.begin ( (obj, res) => { string message = ""; try { var success = shell_extension.install_extension.end (res); if (success) { message = success_message; } } catch (Gnome.ShellExtensionError error) { switch (error.code) { case Gnome.ShellExtensionError.TIMED_OUT: message = _("Time-out reached"); break; case Gnome.ShellExtensionError.NOT_ALLOWED: message = _("Installing extensions is not allowed"); break; case Gnome.ShellExtensionError.DOWNLOAD_FAILED: message = _("Failed to download the extension"); break; default: message = _("Something went wrong"); return; } } if (message != "") { this.window?.add_toast (new Adw.Toast (message)); } this.installing_extension = false; }); } private void update_settings () { var settings = this.shell_extension?.settings; if (this.settings != settings) { this.settings = settings; this.setup (); } } private void on_notify_window (GLib.Object object, GLib.ParamSpec pspec) { if (this.window != null) { this.window.notify["visible-panel"].connect (this.on_notify_visible_panel); } } private void on_notify_settings (GLib.Object object, GLib.ParamSpec pspec) { this.update_settings (); } private void on_notify_available (GLib.Object object, GLib.ParamSpec pspec) { this.setup (); } private void on_notify_visible_panel () { this.setup (); } public override void dispose () { this.taredown (); if (this.window != null) { this.window.notify["visible-panel"].disconnect (this.on_notify_visible_panel); } if (this.shell_extension != null) { this.shell_extension.notify["settings"].disconnect (this.on_notify_settings); this.shell_extension.notify["available"].disconnect (this.on_notify_available); this.shell_extension = null; } this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/screen-overlay-provider.vala000066400000000000000000000047751520625676500277040ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Gnome { /** * Provider for the screen overlay when the extension is enabled. * * Extension manages the overlay. Here we only need support for manually opening the overlay. */ public class ScreenOverlayProvider : Ft.Provider, Ft.ScreenOverlayProvider { private Gnome.ShellExtension? shell_extension = null; private void update_available () { this.available = this.shell_extension != null && this.shell_extension.enabled; } private void on_notify_extension_enabled (GLib.Object object, GLib.ParamSpec pspec) { this.update_available (); } public void open () { var proxy = this.shell_extension?.get_shell_integration_proxy (); if (proxy != null) { proxy.open_screen_overlay.begin ( (obj, res) => { try { proxy.open_screen_overlay.end (res); } catch (GLib.Error error) { GLib.warning ("Error opening screen overlay: %s", error.message); } }); } else { GLib.debug ("Unable to open screen overlay. No ShellIntegration."); } } public void close () { } protected override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.shell_extension = new Gnome.ShellExtension (); this.shell_extension.notify["enabled"].connect (this.on_notify_extension_enabled); this.update_available (); } protected override async void uninitialize () throws GLib.Error { if (this.shell_extension != null) { this.shell_extension.notify["enabled"].disconnect (this.on_notify_extension_enabled); this.shell_extension = null; this.update_available (); } } protected override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { } protected override async void disable () throws GLib.Error { this.close (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/screen-saver-provider.vala000066400000000000000000000063321520625676500273320ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Gnome { public class ScreenSaverProvider : Ft.Provider, Ft.ScreenSaverProvider { public bool active { get { return this._active; } } private Gnome.ScreenSaver? screensaver_proxy = null; private uint watcher_id = 0; private bool _active = false; private ulong active_changed_id = 0; private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_active_changed (bool active) { if (this._active != active) { this._active = active; this.notify_property ("active"); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.gnome.ScreenSaver", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { try { this.screensaver_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SESSION, "org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", GLib.DBusProxyFlags.DO_NOT_AUTO_START, cancellable); this._active = yield this.screensaver_proxy.get_active (); this.active_changed_id = this.screensaver_proxy.active_changed.connect (this.on_active_changed); } catch (GLib.Error error) { GLib.warning ("Error while initializing session proxy: %s", error.message); } } public override async void disable () throws GLib.Error { if (this.active_changed_id != 0) { this.screensaver_proxy.disconnect (this.active_changed_id); this.active_changed_id = 0; } this.screensaver_proxy = null; if (this._active) { this._active = false; this.notify_property ("active"); } } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/shell-extension-settings.vala000066400000000000000000000064111520625676500300620ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { /** * Settings of a `ShellIntegration` may have different lifespan than `ShellExtension`, * therefore settings are represented separately. */ public class ShellExtensionSettings : GLib.Object { public string indicator_type { owned get { return this.proxy != null ? this.proxy.indicator_type : ""; } set { if (this.proxy != null) { this.proxy.indicator_type = value; } } } public bool blur_effect { get { return this.proxy != null ? this.proxy.enable_blur_effect : false; } set { if (this.proxy != null) { this.proxy.enable_blur_effect = value; } } } public bool dismiss_gesture { get { return this.proxy != null ? this.proxy.enable_dismiss_gesture : false; } set { if (this.proxy != null) { this.proxy.enable_dismiss_gesture = value; } } } public bool manage_notifications { get { return this.proxy != null ? this.proxy.enable_manage_notifications : false; } set { if (this.proxy != null) { this.proxy.enable_manage_notifications = value; } } } private Gnome.ShellIntegration? proxy = null; public ShellExtensionSettings (Gnome.ShellIntegration shell_integration_proxy) { this.proxy = shell_integration_proxy; var proxy = (GLib.DBusProxy) shell_integration_proxy; proxy.g_properties_changed.connect (this.on_properties_changed); } private void on_properties_changed (GLib.Variant changed_properties, string[] invalidated_properties) { if (changed_properties.lookup_value ("IndicatorType", null) != null) { this.notify_property ("indicator-type"); } if (changed_properties.lookup_value ("EnableBlurEffect", null) != null) { this.notify_property ("blur_effect"); } if (changed_properties.lookup_value ("EnableDismissGesture", null) != null) { this.notify_property ("dismiss-gesture"); } if (changed_properties.lookup_value ("EnableManageNotifications", null) != null) { this.notify_property ("manage-notifications"); } } public override void dispose () { if (this.proxy != null) { var proxy = (GLib.DBusProxy) this.proxy; proxy.g_properties_changed.disconnect (this.on_properties_changed); this.proxy = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/shell-extension.vala000066400000000000000000000310521520625676500262230ustar00rootroot00000000000000/* * Copyright (c) 2017-2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { public errordomain ShellExtensionError { TIMED_OUT, NOT_ALLOWED, DOWNLOAD_FAILED, OTHER } [SingleInstance] public class ShellExtension : GLib.Object { public const string EXTENSION_UUID = "focus-timer@focustimerhq.github.io"; // When published we suggest to install it public const bool IS_PUBLISHED = false; [CCode (notify = false)] public bool available { get { return this._available; } private set { if (this._available != value) { this._available = value; this.notify_property ("available"); } } } [CCode (notify = false)] public bool enabled { get { return this._enabled; } private set { if (this._enabled != value) { this._enabled = value; this.notify_property ("enabled"); } } } public Gnome.ExtensionState state { get { return this.extension_info.state; } } public Gnome.ShellExtensionSettings? settings { get { return this._settings; } } private bool _available = false; private bool _enabled = false; private Gnome.ShellExtensionSettings? _settings = null; private Gnome.ShellExtensions? shell_extensions_proxy = null; private Gnome.ShellIntegration? shell_integration_proxy = null; private Gnome.ExtensionInfo extension_info; private uint shell_watcher_id = 0; private uint shell_integration_watcher_id = 0; private GLib.Cancellable? cancellable = null; construct { this.extension_info = Gnome.ExtensionInfo (EXTENSION_UUID); this.cancellable = new GLib.Cancellable (); if (!Ft.is_devel ()) { this.shell_watcher_id = GLib.Bus.watch_name ( GLib.BusType.SESSION, "org.gnome.Shell", GLib.BusNameWatcherFlags.NONE, this.on_shell_name_appeared, this.on_shell_name_vanished); this.shell_integration_watcher_id = GLib.Bus.watch_name ( GLib.BusType.SESSION, "io.github.focustimerhq.FocusTimer.ShellIntegration", GLib.BusNameWatcherFlags.NONE, this.on_shell_integration_name_appeared, this.on_shell_integration_name_vanished); } } internal unowned Gnome.ShellIntegration? get_shell_integration_proxy () { return this.shell_integration_proxy; } private void update_available () { this.available = this.shell_extensions_proxy != null && this.shell_extensions_proxy.user_extensions_enabled && this.extension_info.state != Gnome.ExtensionState.UNKNOWN; } private void on_properties_changed (GLib.Variant changed_properties, string[] invalidated_properties) { this.update_available (); } private void on_shell_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (this.shell_extensions_proxy != null) { return; } try { this.shell_extensions_proxy = GLib.Bus.get_proxy_sync ( GLib.BusType.SESSION, "org.gnome.Shell", "/org/gnome/Shell", GLib.DBusProxyFlags.DO_NOT_AUTO_START, this.cancellable); var shell_extensions_proxy = (GLib.DBusProxy) this.shell_extensions_proxy; shell_extensions_proxy.g_properties_changed.connect (this.on_properties_changed); this.shell_extensions_proxy.extension_state_changed.connect ( this.on_extension_state_changed); this.query_extension_state.begin (); } catch (GLib.Error error) { GLib.warning ("Error while initializing extensions proxy: %s", error.message); } } private void on_shell_name_vanished (GLib.DBusConnection? connection, string name) { if (this.shell_extensions_proxy == null) { return; } var shell_extensions_proxy = (GLib.DBusProxy) this.shell_extensions_proxy; shell_extensions_proxy.g_properties_changed.disconnect (this.on_properties_changed); this.shell_extensions_proxy.extension_state_changed.disconnect ( this.on_extension_state_changed); this.extension_info = Gnome.ExtensionInfo (EXTENSION_UUID); this.notify_property ("state"); this.shell_extensions_proxy = null; this.available = false; this.enabled = false; } private void on_shell_integration_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (this.shell_integration_proxy != null) { return; } try { this.shell_integration_proxy = GLib.Bus.get_proxy_sync ( GLib.BusType.SESSION, "io.github.focustimerhq.FocusTimer.ShellIntegration", "/io/github/focustimerhq/FocusTimer/ShellIntegration", GLib.DBusProxyFlags.DO_NOT_AUTO_START, this.cancellable); this._settings = new Gnome.ShellExtensionSettings (this.shell_integration_proxy); this.notify_property ("settings"); } catch (GLib.Error error) { GLib.warning ("Error while initializing Shell integration proxy: %s", error.message); } } private void on_shell_integration_name_vanished (GLib.DBusConnection? connection, string name) { if (this._settings != null) { this._settings = null; this.notify_property ("settings"); } this.shell_integration_proxy = null; } private void on_extension_state_changed (string uuid, GLib.HashTable state) { if (uuid != EXTENSION_UUID) { return; } this.extension_info = Gnome.ExtensionInfo.deserialize (uuid, state); this.enabled = this.extension_info.enabled; this.update_available (); this.notify_property ("state"); } private async void query_extension_state () { try { this.extension_info = Gnome.ExtensionInfo.deserialize ( EXTENSION_UUID, yield this.shell_extensions_proxy.get_extension_info (EXTENSION_UUID)); this.enabled = this.extension_info.enabled; this.notify_property ("state"); } catch (GLib.Error error) { GLib.warning ("Error while querying extension state: %s", error.message); } this.update_available (); } public bool is_installed () { if (this.extension_info.state == Gnome.ExtensionState.UNINSTALLED) { return false; } return this.extension_info.path != ""; } public bool has_update () { return this.extension_info.has_update; } public async bool enable_extension () { if (this.shell_extensions_proxy == null) { return false; } try { return yield this.shell_extensions_proxy.enable_extension (EXTENSION_UUID); } catch (GLib.Error error) { GLib.warning ("Error while enabling extension: %s", error.message); return false; } } public async bool disable_extension () { if (this.shell_extensions_proxy == null) { return false; } try { return yield this.shell_extensions_proxy.disable_extension (EXTENSION_UUID); } catch (GLib.Error error) { GLib.warning ("Error while disabling extension: %s", error.message); return false; } } public async bool install_extension () throws Gnome.ShellExtensionError { if (this.shell_extensions_proxy == null) { return false; } try { var result = yield this.shell_extensions_proxy.install_remote_extension (EXTENSION_UUID); switch (result) { case "successful": return true; case "cancelled": return false; default: GLib.warning ("Unhandled InstallRemoteExtension result: `%s`", result); return false; } } catch (GLib.IOError error) { if (error.code == GLib.IOError.TIMED_OUT) { throw new Gnome.ShellExtensionError.TIMED_OUT ("Timed out"); } if (error.code == GLib.IOError.DBUS_ERROR && error.message.contains ("Shell.Extensions.Error.NotAllowed")) { throw new Gnome.ShellExtensionError.NOT_ALLOWED ("Not allowed"); } if (error.code == GLib.IOError.DBUS_ERROR && error.message.contains ("Shell.Extensions.Error.InfoDownloadFailed") || error.message.contains ("Shell.Extensions.Error.DownloadFailed")) { throw new Gnome.ShellExtensionError.DOWNLOAD_FAILED ("Failed to download the extension"); } GLib.warning ("Error while installing extension: %s", error.message); throw new Gnome.ShellExtensionError.OTHER (error.message); } catch (GLib.Error error) { GLib.warning ("Error while installing extension: %s", error.message); throw new Gnome.ShellExtensionError.OTHER (error.message); } } public async bool uninstall_extension () { if (this.shell_extensions_proxy == null) { return false; } try { return yield this.shell_extensions_proxy.uninstall_extension (EXTENSION_UUID); } catch (GLib.Error error) { GLib.warning ("Error while uninstalling extension: %s", error.message); return false; } } public override void dispose () { if (this.shell_watcher_id != 0) { GLib.Bus.unwatch_name (this.shell_watcher_id); this.shell_watcher_id = 0; } if (this.shell_integration_watcher_id != 0) { GLib.Bus.unwatch_name (this.shell_integration_watcher_id); this.shell_integration_watcher_id = 0; } if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } this._settings = null; this.shell_extensions_proxy = null; this.shell_integration_proxy = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/gnome/window-extension.vala000066400000000000000000000070741520625676500264320ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Gnome { public class WindowExtension : Ft.WindowExtension { private Gnome.ShellExtension? shell_extension = null; private Adw.Toast? install_extension_toast = null; private static bool install_extension_toast_dismissed = false; construct { this.shell_extension = new Gnome.ShellExtension (); this.shell_extension.notify["available"].connect (this.on_extension_notify_available); this.notify["window"].connect (this.on_notify_window); } private void show_install_extension_toast () { if (Gnome.WindowExtension.install_extension_toast_dismissed || this.install_extension_toast != null || this.window == null) { return; } var toast = new Adw.Toast (_("GNOME Shell extension available")); toast.button_label = _("Learn More"); toast.priority = Adw.ToastPriority.HIGH; toast.timeout = 0; toast.button_clicked.connect ( () => { var dialog = new Gnome.InstallExtensionDialog (); dialog.present (this.window); this.install_extension_toast = null; }); toast.dismissed.connect (this.on_install_extension_toast_dismissed); this.install_extension_toast = toast; this.window.add_toast (toast); } private void update_install_extension_toast () { if (!Gnome.ShellExtension.IS_PUBLISHED) { return; } if (this.window == null || !this.window.get_mapped ()) { return; } if (this.shell_extension.available && !this.shell_extension.is_installed ()) { this.show_install_extension_toast (); } else if (this.install_extension_toast != null) { this.install_extension_toast.dismissed.disconnect ( this.on_install_extension_toast_dismissed); this.install_extension_toast.dismiss (); this.install_extension_toast = null; } } private void on_notify_window (GLib.Object object, GLib.ParamSpec pspec) { if (this.window != null) { this.window.map.connect (this.on_map); } } private void on_map () { this.update_install_extension_toast (); } private void on_extension_notify_available (GLib.Object object, GLib.ParamSpec pspec) { this.update_install_extension_toast (); } private void on_install_extension_toast_dismissed (Adw.Toast toast) { this.install_extension_toast = null; Gnome.WindowExtension.install_extension_toast_dismissed = true; } public override void dispose () { if (this.window != null) { this.window.map.disconnect (this.on_map); } if (this.shell_extension != null) { this.shell_extension.notify["available"].disconnect (this.on_extension_notify_available); this.shell_extension = null; } this.install_extension_toast = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/kde/000077500000000000000000000000001520625676500216725ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/kde/data/000077500000000000000000000000001520625676500226035ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/kde/data/io.github.focustimerhq.FocusTimer.notifyrc000066400000000000000000000010511520625676500330360ustar00rootroot00000000000000[Global] Name=Focus Timer DesktopEntry=io.github.focustimerhq.FocusTimer [Event/time-block-started] Name=Countdown started Action=Popup Urgency=High [Event/time-block-running] Name=Countdown state Action=Popup Urgency=High [Event/time-block-about-to-end] Name=Announce countdown is about to end Action=Popup Urgency=Critical [Event/time-block-ended] Name=Countdown finished Action=Popup Urgency=Critical [Event/confirm-advancement] Name=Confirm advancement Action=Popup Urgency=Critical [Event/action-failed] Name=Custom action error Urgency=High focustimerhq-FocusTimer-8581be2/src/plugins/kde/data/meson.build000066400000000000000000000001711520625676500247440ustar00rootroot00000000000000install_data( 'io.github.focustimerhq.FocusTimer.notifyrc', install_dir: get_option('datadir') / 'knotifications6', )focustimerhq-FocusTimer-8581be2/src/plugins/kde/kde.gresource.xml000066400000000000000000000002211520625676500251470ustar00rootroot00000000000000 kde.plugin focustimerhq-FocusTimer-8581be2/src/plugins/kde/kde.plugin000066400000000000000000000001331520625676500236520ustar00rootroot00000000000000[Plugin] Name=KDE Module=kde Builtin=true Embedded=kde_peas_register_types X-Priority=high focustimerhq-FocusTimer-8581be2/src/plugins/kde/kde.vala000066400000000000000000000010131520625676500232750ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Kde { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; if (Ft.get_desktop_name () != "kde") { return; } object_module.register_extension_type (typeof (Ft.PreferencesWindowExtension), typeof (Kde.PreferencesWindowExtension)); } } focustimerhq-FocusTimer-8581be2/src/plugins/kde/meson.build000066400000000000000000000007301520625676500240340ustar00rootroot00000000000000if get_option('plugin_kde').enabled() kde_plugin_resources = gnome.compile_resources( 'kde-plugin-resources', 'kde.gresource.xml', c_name: 'kde_plugin', ) libft_plugin_kde = static_library( 'ft_plugin_kde', files( 'kde.vala', 'preferences-window-extension.vala', ) + kde_plugin_resources, dependencies: [libft_ui_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_kde] subdir('data') endif focustimerhq-FocusTimer-8581be2/src/plugins/kde/preferences-window-extension.vala000066400000000000000000000106371520625676500303660ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Kde { public class PreferencesWindowExtension : Ft.PreferencesWindowExtension { private Ft.PreferencesPanel? last_panel = null; private unowned Adw.PreferencesGroup? notification_group = null; construct { this.notify["window"].connect (this.on_notify_window); } private void setup_notifications_panel (Ft.PreferencesPanel panel) { var page = panel.get_preferences_page (); if (this.notification_group == null) { // translators: abbreviate it to just "Settings" if it gets too long var notification_settings_button = new Gtk.Button.with_label (_("Open Settings")); notification_settings_button.valign = Gtk.Align.CENTER; notification_settings_button.margin_start = 12; notification_settings_button.clicked.connect ( () => { try { if (Ft.is_flatpak ()) { GLib.AppInfo.launch_default_for_uri ( "systemsettings://kcm_notifications", null); } else { var app_info = GLib.AppInfo.create_from_commandline ( @"kcmshell6 kcm_notifications --args --desktop-entry=$(Config.APPLICATION_ID)", null, GLib.AppInfoCreateFlags.NONE); app_info.launch (null, null); } } catch (GLib.Error error) { GLib.warning ("Error opening notification settings: %s", error.message); } }); var notification_group = new Adw.PreferencesGroup (); notification_group.description = _("For reliable break reminders, allow this app's notifications during Do Not Disturb and disable its notification history."); notification_group.header_suffix = notification_settings_button; page.add (notification_group); notification_group.insert_after (notification_group.parent, null); // move to top this.notification_group = notification_group; } } private void taredown_notifications_panel (Ft.PreferencesPanel panel) { this.notification_group?.unparent (); this.notification_group = null; } /** * Modify visible_panel of the PreferencesWindow. */ private void setup () { var panel = this.window?.visible_panel; if (panel != this.last_panel) { switch (this.last_panel?.tag) { case "notifications": this.taredown_notifications_panel (this.last_panel); break; } this.last_panel = panel; } switch (panel?.tag) { case "notifications": this.setup_notifications_panel (panel); break; default: this.taredown (); break; } } private void taredown () { if (this.last_panel == null) { return; } this.taredown_notifications_panel (this.last_panel); this.last_panel = null; } private void on_notify_window (GLib.Object object, GLib.ParamSpec pspec) { if (this.window != null) { this.window.notify["visible-panel"].connect (this.on_notify_visible_panel); } } private void on_notify_visible_panel () { this.setup (); } public override void dispose () { this.taredown (); if (this.window != null) { this.window.notify["visible-panel"].disconnect (this.on_notify_visible_panel); } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/meson.build000066400000000000000000000002101520625676500232620ustar00rootroot00000000000000libft_plugins = [] subdir('freedesktop') subdir('portal') subdir('gnome') subdir('kde') subdir('sni') subdir('wayland') subdir('xfce') focustimerhq-FocusTimer-8581be2/src/plugins/portal/000077500000000000000000000000001520625676500224305ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/portal/background-provider.vala000066400000000000000000000171471520625676500272560ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Portal { public class BackgroundProvider : Ft.Provider, Ft.BackgroundProvider { /** * Warn if underlying `Background` API version changes. Bump this value after testing. */ private const uint COMAPTIBLE_VERSION = 2U; private const string[] COMMANDLINE = {"focus-timer", "--gapplication-service"}; public Ft.Priority priority { get { return Ft.Priority.HIGH; } } public new bool background_allowed { get { return this._background_allowed; } } public new bool autostart_allowed { get { return this._autostart_allowed; } } private GLib.DBusConnection? connection = null; private Portal.Background? proxy = null; private GLib.Cancellable? cancellable = null; private GLib.HashTable requests = null; private uint dbus_watcher_id = 0U; private bool _background_allowed = false; private bool _autostart_allowed = false; private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; this.connection = connection; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; this.connection = null; } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.requests = new GLib.HashTable (GLib.direct_hash, GLib.direct_equal); if (this.dbus_watcher_id == 0) { this.dbus_watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { if (this.proxy != null) { return; } this.cancellable = cancellable != null ? cancellable : new GLib.Cancellable (); try { this.proxy = yield GLib.Bus.get_proxy (GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", GLib.DBusProxyFlags.NONE, this.cancellable); if (this.proxy.version > COMAPTIBLE_VERSION) { GLib.warning ("Using Background API version %u. Implementation was aimed for older version.", this.proxy.version); } } catch (GLib.Error error) { GLib.warning ("Error while enabling background provider: %s", error.message); throw error; } } public override async void disable () throws GLib.Error { if (this.cancellable != null) { this.cancellable.cancel (); } // XXX: the request does not get withdrawn // if (this._autostart_allowed) { // this._autostart_allowed = false; // this.notify_property ("autostart-allowed"); // } // // if (this._background_allowed) { // this._background_allowed = false; // this.notify_property ("background-allowed"); // } this.proxy = null; this.requests = null; } public override async void uninitialize () throws GLib.Error { if (this.dbus_watcher_id != 0) { GLib.Bus.unwatch_name (this.dbus_watcher_id); this.dbus_watcher_id = 0; } this.cancellable = null; } public async void request_background (bool autostart, string parent_window) { string handle_token; try { handle_token = yield Portal.create_request ( this.connection, (response, results) => { if (results != null) { var background_variant = results.lookup ("background"); var autostart_variant = results.lookup ("autostart"); var background_allowed = background_variant != null ? background_variant.get_boolean () : this._background_allowed; var autostart_allowed = autostart_variant != null ? autostart_variant.get_boolean () : this._autostart_allowed; if (autostart_allowed != autostart) { GLib.warning ("Failed to set `autostart = %s`", autostart.to_string ()); } if (this._background_allowed != background_allowed) { this._background_allowed = background_allowed; this.notify_property ("background-allowed"); } if (this._autostart_allowed != autostart_allowed) { this._autostart_allowed = autostart_allowed; this.notify_property ("autostart-allowed"); } } this.request_background.callback (); }); } catch (GLib.Error error) { GLib.warning ("Error while requesting background: %s", error.message); return; } var options = new GLib.HashTable (GLib.str_hash, GLib.str_equal); options.insert ("handle_token", new GLib.Variant.string (handle_token)); options.insert ("autostart", new GLib.Variant.boolean (autostart)); options.insert ("commandline", new GLib.Variant.strv (COMMANDLINE)); this.proxy.request_background.begin ( parent_window, options, (obj, res) => { try { this.proxy.request_background.end (res); } catch (GLib.Error error) { GLib.warning ("Error while requesting background: %s", error.message); } }); yield; // wait for response } } } focustimerhq-FocusTimer-8581be2/src/plugins/portal/global-shortcuts-provider.vala000066400000000000000000000504651520625676500304330ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Portal { private errordomain GlobalShortcutsError { REQUEST, CREATE_SESSION, BIND_SHORTCUTS, CONFIGURE_SHORTCUTS, LIST_SHORTCUTS } public class GlobalShortcutsProvider : Ft.Provider, Ft.GlobalShortcutsProvider { /** * Warn if underlying `GlobalShortcuts` API version changes. Bump this value after testing. */ private const uint COMAPTIBLE_VERSION = 0; private GLib.DBusConnection? connection = null; private Portal.GlobalShortcuts? proxy = null; private Portal.Shortcut[] shortcuts = null; private GLib.Cancellable? cancellable = null; private GLib.ObjectPath? session_handle = null; private GLib.HashTable? accelerators = null; private uint dbus_watcher_id = 0U; private uint bind_shortcuts_idle_id = 0U; private bool is_configured = false; private void mark_as_configured () { if (!this.is_configured) { var settings = Ft.get_settings (); settings.set_boolean ("global-shortcuts-configured", true); this.is_configured = true; } } private async void create_session () throws GlobalShortcutsError { var timestamp = Ft.Timestamp.to_seconds_uint32 (Ft.Timestamp.from_now ()); try { var handle_token = yield Portal.create_request ( this.connection, (response, results) => { if (results != null) { var session_handle_variant = results.lookup ("session_handle"); if (session_handle_variant != null) { this.session_handle = new GLib.ObjectPath ( session_handle_variant.get_string ()); } } this.create_session.callback (); }); var options = new GLib.HashTable (GLib.str_hash, GLib.str_equal); options.insert ("handle_token", new GLib.Variant.string (handle_token)); options.insert ("session_handle_token", new GLib.Variant.string (@"focustimer_$(timestamp)")); yield this.proxy.create_session (options); yield; // wait for response } catch (GLib.Error error) { throw new GlobalShortcutsError.CREATE_SESSION (error.message); } if (this.session_handle == null) { throw new GlobalShortcutsError.CREATE_SESSION ("No session_handle in response"); } } private string parse_trigger_description (string? trigger_description) { if (trigger_description != null) { var position = trigger_description.index_of (" <"); return position > 0 ? trigger_description.slice (position + 1, trigger_description.length) : ""; } return ""; } private Portal.Shortcut[] parse_shortcuts (GLib.Variant shortcuts_variant) { GLib.debug ("Parsing shortcuts... %s", shortcuts_variant.print (false)); var shortcuts = new Portal.Shortcut[0]; var shortcuts_iterator = shortcuts_variant.iterator (); GLib.Variant? tuple_variant; while ((tuple_variant = shortcuts_iterator.next_value ()) != null) { var shortcut_id = tuple_variant.get_child_value (0).get_string (); var properties_variant = tuple_variant.get_child_value (1); var properties_iterator = properties_variant.iterator (); var properties = new GLib.HashTable ( GLib.str_hash, GLib.str_equal); string key; GLib.Variant variant; while (properties_iterator.next ("{sv}", out key, out variant)) { properties.insert (key, variant); } shortcuts += Portal.Shortcut () { id = shortcut_id, properties = properties }; } return shortcuts; } private void update_accelerators (GLib.Variant shortcuts_variant) { var changed_ids = new GLib.GenericSet (GLib.str_hash, GLib.str_equal); var new_accelerators = new GLib.HashTable (GLib.str_hash, GLib.str_equal); var is_initialized = this.accelerators != null; foreach (var shortcut in this.shortcuts) { if (this.accelerators != null && this.accelerators.contains (shortcut.id) && this.accelerators.lookup (shortcut.id) != "") { changed_ids.add (shortcut.id); } new_accelerators.insert (shortcut.id, ""); } foreach (var shortcut in this.parse_shortcuts (shortcuts_variant)) { var existing_accelerator = this.accelerators != null && this.accelerators.contains (shortcut.id) ? this.accelerators.lookup (shortcut.id) : ""; var accelerator = this.parse_trigger_description ( shortcut.properties.lookup ("trigger_description").get_string ()); if (accelerator == existing_accelerator) { changed_ids.remove (shortcut.id); } else if (accelerator != "") { changed_ids.add (shortcut.id); } new_accelerators.insert (shortcut.id, accelerator); } this.accelerators = new_accelerators; if (is_initialized) { changed_ids.@foreach ( (shortcut_id) => { this.accelerator_changed (shortcut_id); }); } } /** * `ListShortcuts` lists shortcuts if they have been changed during the session. * * We use it to update `this.accelerators`. */ private async void list_shortcuts () throws GlobalShortcutsError requires (this.session_handle != null) { string handle_token; GLib.Variant? shortcuts_variant = null; try { handle_token = yield Portal.create_request ( this.connection, (response, results) => { shortcuts_variant = results != null ? results.lookup ("shortcuts") : null; this.list_shortcuts.callback (); }); } catch (GLib.Error error) { throw new GlobalShortcutsError.REQUEST (error.message); } try { var options = new GLib.HashTable (GLib.str_hash, GLib.str_equal); options.insert ("handle_token", new GLib.Variant.string (handle_token)); yield this.proxy.list_shortcuts (this.session_handle, options); yield; // wait for response } catch (GLib.Error error) { throw new GlobalShortcutsError.LIST_SHORTCUTS (error.message); } if (shortcuts_variant != null) { this.update_accelerators (shortcuts_variant); } } private async void bind_shortcuts () throws GlobalShortcutsError { string handle_token; try { handle_token = yield Portal.create_request ( this.connection, (response, results) => { this.bind_shortcuts.callback (); }); } catch (GLib.Error error) { throw new GlobalShortcutsError.REQUEST (error.message); } try { var options = new GLib.HashTable (GLib.str_hash, GLib.str_equal); options.insert ("handle_token", new GLib.Variant.string (handle_token)); yield this.proxy.bind_shortcuts (this.session_handle, this.shortcuts, "", options); yield; // wait for response } catch (GLib.Error error) { throw new GlobalShortcutsError.BIND_SHORTCUTS (error.message); } // Update accelerators if (this.accelerators == null) { yield this.list_shortcuts (); } } private void schedule_bind_shortcuts () { if (this.bind_shortcuts_idle_id != 0) { return; } this.bind_shortcuts_idle_id = GLib.Idle.add ( () => { this.bind_shortcuts_idle_id = 0; this.bind_shortcuts.begin ( (obj, res) => { try { this.bind_shortcuts.end (res); } catch (GLib.Error error) { GLib.warning ("Error while binding shortcuts: %s", error.message); } }); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.bind_shortcuts_idle_id, "Portal.GlobalShortcutsProvider.bind_shortcuts"); } /** * `BindShortcuts` only displays a dialog if there are new entries. To force display * a dialog we add an "Unsed" shortcut. */ private Portal.Shortcut[] mutulate_shortcuts () { var timestamp = Ft.Timestamp.to_seconds_uint32 (Ft.Timestamp.from_now ()); var shortcut_id = @"unused-$(timestamp)"; var shortcut_properties = new GLib.HashTable (GLib.str_hash, GLib.str_equal); shortcut_properties.insert ("description", new GLib.Variant.string (_("Unused"))); var shortcuts = this.shortcuts.copy (); shortcuts += Portal.Shortcut () { id = shortcut_id, properties = shortcut_properties }; return shortcuts; } private async void open_global_shortcuts_dialog_async (string window_identifier) throws GlobalShortcutsError { string handle_token; if (this.proxy == null || this.shortcuts.length == 0) { return; } // HACK: It's possible to open the dialog only once per session. yield this.create_session (); try { handle_token = yield Portal.create_request ( this.connection, (response, results) => { this.open_global_shortcuts_dialog_async.callback (); }); } catch (GLib.Error error) { throw new GlobalShortcutsError.REQUEST (error.message); } var options = new GLib.HashTable (GLib.str_hash, GLib.str_equal); options.insert ("handle_token", new GLib.Variant.string (handle_token)); if (this.proxy.version >= 2 && this.is_configured) { try { yield this.proxy.configure_shortcuts (this.session_handle, window_identifier, options); } catch (GLib.Error error) { yield new GlobalShortcutsError.CONFIGURE_SHORTCUTS (error.message); } } else { try { yield this.proxy.bind_shortcuts (this.session_handle, this.mutulate_shortcuts (), window_identifier, options); } catch (GLib.Error error) { yield new GlobalShortcutsError.BIND_SHORTCUTS (error.message); } } yield; // wait for response } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (has_dbus_interface (connection, name, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.GlobalShortcuts")) { this.available = true; this.connection = connection; } } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; this.connection = null; } private void on_activated (GLib.ObjectPath session_handle, string shortcut_id, uint64 timestamp, GLib.HashTable options) { this.shortcut_activated (shortcut_id); } private void on_shortcuts_changed (GLib.ObjectPath session_handle, Portal.Shortcut[] shortcuts) { if (this.accelerators != null) { this.list_shortcuts.begin ( (obj, res) => { try { this.list_shortcuts.end (res); } catch (GLib.Error error) { GLib.warning ("Error while listing shortcuts: %s", error.message); } }); } if (shortcuts.length > 0) { this.mark_as_configured (); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.shortcuts = new Portal.Shortcut[0]; if (this.dbus_watcher_id == 0) { this.dbus_watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { if (this.proxy != null) { return; } this.cancellable = cancellable != null ? cancellable : new GLib.Cancellable (); this.is_configured = Ft.get_settings ().get_boolean ("global-shortcuts-configured"); try { this.proxy = yield GLib.Bus.get_proxy (GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", GLib.DBusProxyFlags.NONE, this.cancellable); this.proxy.activated.connect (this.on_activated); this.proxy.shortcuts_changed.connect (this.on_shortcuts_changed); if (this.proxy.version > COMAPTIBLE_VERSION) { GLib.warning ("Using GlobalShortcuts API version %u. Implementation was aimed for older version.", this.proxy.version); } yield this.create_session (); } catch (GLib.Error error) { GLib.warning ("Error while creating global shortcuts session: %s", error.message); throw error; } } public override async void disable () throws GLib.Error { if (this.cancellable != null) { this.cancellable.cancel (); } if (this.bind_shortcuts_idle_id != 0) { GLib.Source.remove (this.bind_shortcuts_idle_id); this.bind_shortcuts_idle_id = 0; } if (this.proxy != null) { this.proxy.activated.disconnect (this.on_activated); this.proxy.shortcuts_changed.disconnect (this.on_shortcuts_changed); this.proxy = null; } this.session_handle = null; } public override async void uninitialize () throws GLib.Error { if (this.dbus_watcher_id != 0) { GLib.Bus.unwatch_name (this.dbus_watcher_id); this.dbus_watcher_id = 0; } this.cancellable = null; this.shortcuts = null; this.accelerators = null; } public void add_shortcut (string name, string description, string default_accelerator = "") requires (this.session_handle != null) { var shortcut_properties = new GLib.HashTable (GLib.str_hash, GLib.str_equal); shortcut_properties.insert ("description", new GLib.Variant.string (description)); if (default_accelerator != "") { shortcut_properties.insert ("preferred_trigger", new GLib.Variant.string (default_accelerator)); } var shortcut = Portal.Shortcut () { id = name, properties = shortcut_properties }; this.shortcuts += shortcut; // We need to bind shortcuts for the `activate` signal to work, even if they have been // configured before. Binding shortcuts may show a dialog. We want to prevent the dialog // from showing up on app start. if (this.is_configured) { this.schedule_bind_shortcuts (); } } public string lookup_accelerator (string name) { if (this.accelerators == null) { return ""; } var accelerator = this.accelerators.lookup (name); return accelerator != null ? accelerator : ""; } public void open_global_shortcuts_dialog (string window_identifier) { this.open_global_shortcuts_dialog_async.begin ( window_identifier, (obj, res) => { try { this.open_global_shortcuts_dialog_async.end (res); } catch (GLib.Error error) { GLib.warning ("Error opening shortcuts dialog: %s", error.message); } }); } } } focustimerhq-FocusTimer-8581be2/src/plugins/portal/interfaces.vala000066400000000000000000000126441520625676500254270ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Portal { [DBus (name = "((s{sv}))")] public struct Shortcut { public string id; public GLib.HashTable properties; } [DBus (name = "org.freedesktop.portal.Request")] public interface Request : GLib.Object { public abstract void close () throws GLib.DBusError, GLib.IOError; public signal void response (uint32 response, GLib.HashTable results); } [DBus (name = "org.freedesktop.portal.Background")] public interface Background : GLib.Object { public abstract uint32 version { get; } public abstract async GLib.ObjectPath request_background (string parent_window, GLib.HashTable options) throws GLib.DBusError, GLib.IOError; } [DBus (name = "org.freedesktop.portal.GlobalShortcuts")] public interface GlobalShortcuts : GLib.Object { public abstract uint32 version { get; } public abstract async GLib.ObjectPath create_session (GLib.HashTable options) throws GLib.DBusError, GLib.IOError; public abstract async GLib.ObjectPath bind_shortcuts (GLib.ObjectPath session_handle, Shortcut[] shortcuts, string parent_window, GLib.HashTable options) throws GLib.DBusError, GLib.IOError; public abstract async GLib.ObjectPath list_shortcuts (GLib.ObjectPath session_handle, GLib.HashTable options) throws GLib.DBusError, GLib.IOError; public abstract async void configure_shortcuts (GLib.ObjectPath session_handle, string parent_window, GLib.HashTable options) throws GLib.DBusError, GLib.IOError; public signal void activated (GLib.ObjectPath session_handle, string shortcut_id, uint64 timestamp, GLib.HashTable options); public signal void deactivated (GLib.ObjectPath session_handle, string shortcut_id, uint64 timestamp, GLib.HashTable options); public signal void shortcuts_changed (GLib.ObjectPath session_handle, Shortcut[] shortcuts); } /** * https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Notification.html */ [DBus (name = "org.freedesktop.portal.Notification")] public interface Notification : GLib.Object { public abstract GLib.HashTable supported_options { owned get; } public abstract uint32 version { get; } public abstract async void add_notification (string id, GLib.HashTable notification, GLib.Cancellable? cancellable) throws GLib.DBusError, GLib.IOError; public abstract async void remove_notification (string id) throws GLib.DBusError, GLib.IOError; public signal void action_invoked (string id, string action, GLib.Variant[] parameter); } internal bool has_dbus_interface (GLib.DBusConnection connection, string bus_name, string object_path, string interface_name, int timeout = -1) { try { var result = connection.call_sync ( bus_name, object_path, "org.freedesktop.DBus.Introspectable", "Introspect", null, new GLib.VariantType ("(s)"), GLib.DBusCallFlags.NONE, timeout); string xml_data; result.get ("(s)", out xml_data); var node_info = new GLib.DBusNodeInfo.for_xml (xml_data); foreach (var iface in node_info.interfaces) { if (iface.name == interface_name) { return true; } } } catch (GLib.Error error) { GLib.debug ("Failed to introspect %s: %s", bus_name, error.message); } return false; } } focustimerhq-FocusTimer-8581be2/src/plugins/portal/meson.build000066400000000000000000000010351520625676500245710ustar00rootroot00000000000000portal_plugin_resources = gnome.compile_resources( 'portal-plugin-resources', 'portal.gresource.xml', c_name: 'portal_plugin', ) libft_plugin_portal = static_library( 'ft_plugin_portal', files( 'interfaces.vala', 'portal.vala', 'request.vala', 'background-provider.vala', 'global-shortcuts-provider.vala', 'notification-backend-provider.vala', ) + portal_plugin_resources, dependencies: [libft_core_dep, gio_dep, peas_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_portal]focustimerhq-FocusTimer-8581be2/src/plugins/portal/notification-backend-provider.vala000066400000000000000000000227131520625676500312050ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Portal { /** * Return whether notifications portal is "on par" with Freedesktop one. */ private bool have_notifications_portal () { switch (Ft.get_desktop_name ()) { case "gnome": return !Ft.is_devel (); default: return false; } } public class NotificationBackendProvider : Ft.Provider, Ft.NotificationBackendProvider { public Ft.Priority priority { get { return Ft.Priority.HIGH; } } private uint watcher_id = 0; private Portal.Notification? proxy = null; private GLib.HashTable? notifications = null; private GLib.Cancellable? cancellable = null; private GLib.Application? application; construct { this.application = GLib.Application.get_default (); } private bool activate_action (string? action, GLib.Variant? parameter) { if (parameter != null && parameter.is_floating ()) { return false; } if (action != null && action.has_prefix ("app.")) { var action_name = action.split (".", 2)[1]; GLib.VariantType? parameter_type = null; var action_group = (GLib.ActionGroup) this.application; if (action_group.query_action (action_name, null, out parameter_type, null, null, null) && ((parameter_type == null && parameter == null) || (parameter_type != null && parameter != null && parameter.is_of_type (parameter_type)))) { action_group.activate_action (action_name, parameter); return true; } } else if (action == null) { this.application.activate (); return true; } return false; } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (has_dbus_interface (connection, name, "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Notification")) { this.available = true; } } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_action_invoked (string id, string action, GLib.Variant[] parameter) { GLib.Variant? target_value = parameter.length >= 1 ? parameter[0] : null; this.activate_action (action, target_value); } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { if (!have_notifications_portal ()) { this.available = false; return; } this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { if (this.proxy != null) { return; } this.cancellable = cancellable != null ? cancellable : new GLib.Cancellable (); this.notifications = new GLib.HashTable (GLib.str_hash, GLib.str_equal); try { this.proxy = yield GLib.Bus.get_proxy ( GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", GLib.DBusProxyFlags.DO_NOT_AUTO_START, this.cancellable); this.proxy.action_invoked.connect (this.on_action_invoked); } catch (GLib.Error error) { GLib.warning ("Error while creating global shortcuts session: %s", error.message); throw error; } } public override async void disable () throws GLib.Error { this.cancellable?.cancel (); if (this.proxy != null) { this.proxy.action_invoked.disconnect (this.on_action_invoked); this.proxy = null; } this.notifications = null; this.cancellable = null; } public async void send_notification (string id, Ft.Notification notification) requires (this.proxy != null) { if (this.cancellable == null || this.cancellable.is_cancelled ()) { return; } unowned var existing_notification = this.notifications.lookup (id); if (existing_notification != null && existing_notification.is_similar (notification)) { return; } var notification_properties = new GLib.HashTable (GLib.str_hash, GLib.str_equal); notification_properties.insert ("title", new GLib.Variant.string (notification.title)); notification_properties.insert ("body", new GLib.Variant.string (notification.body)); notification_properties.insert ("priority", new GLib.Variant.string (notification.priority.to_string ())); // TODO: define serialize_property if (notification.icon != null) { // TODO } if (notification.is_transient) { notification_properties.insert ("display-hint", new GLib.Variant.strv ({ "transient", })); } if (notification.suppress_sound) { notification_properties.insert ("sound", new GLib.Variant.string ("silent")); } if (notification.default_action != null) { notification_properties.insert ( "default-action", new GLib.Variant.string (notification.default_action)); } if (notification.default_target_value != null) { notification_properties.insert ( "default-action-target", notification.default_target_value); } var buttons = new GLib.Variant[0]; notification.foreach_button ( (label, action, target_value) => { var button = new GLib.VariantBuilder (GLib.VariantType.VARDICT); button.add ("{sv}", "label", new GLib.Variant.string (label)); button.add ("{sv}", "action", new GLib.Variant.string (action)); if (target_value != null) { button.add ("{sv}", "target", target_value); } buttons += button.end (); }); if (buttons.length > 0) { notification_properties.insert ( "buttons", new GLib.Variant.array (GLib.VariantType.VARDICT, buttons)); } try { yield this.proxy.add_notification ( id, notification_properties, this.cancellable); } catch (GLib.Error error) { GLib.warning ("Failed to add notification [%s.%d]: %s", error.domain.to_string (), error.code, error.message); } } public async void withdraw_notification (string id) requires (this.proxy != null) { if (this.cancellable == null || this.cancellable.is_cancelled ()) { return; } this.notifications.remove (id); try { yield this.proxy.remove_notification (id); } catch (GLib.Error error) { GLib.warning ("Failed to remove notification [%s.%d]: %s", error.domain.to_string (), error.code, error.message); } } public override void dispose () { this.application = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/portal/portal.gresource.xml000066400000000000000000000002271520625676500264510ustar00rootroot00000000000000 portal.plugin focustimerhq-FocusTimer-8581be2/src/plugins/portal/portal.plugin000066400000000000000000000001471520625676500251530ustar00rootroot00000000000000[Plugin] Name=Portal Module=portal Builtin=true Embedded=portal_peas_register_types X-Priority=default focustimerhq-FocusTimer-8581be2/src/plugins/portal/portal.vala000066400000000000000000000014271520625676500246020ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Portal { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; object_module.register_extension_type (typeof (Ft.BackgroundProvider), typeof (Portal.BackgroundProvider)); object_module.register_extension_type (typeof (Ft.GlobalShortcutsProvider), typeof (Portal.GlobalShortcutsProvider)); object_module.register_extension_type (typeof (Ft.NotificationBackendProvider), typeof (Portal.NotificationBackendProvider)); } } focustimerhq-FocusTimer-8581be2/src/plugins/portal/request.vala000066400000000000000000000034451520625676500247730ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Portal { private uint next_request_id = 1U; private GLib.HashTable requests = null; public delegate void RequestCallback (uint32 response, GLib.HashTable results); public async string create_request (GLib.DBusConnection connection, Portal.RequestCallback callback) throws GLib.Error { var request_id = next_request_id; var handle_token = "focustimer_" + request_id.to_string (); var sender = connection.get_unique_name (); var sender_escaped = sender.replace (":", "").replace (".", "_"); next_request_id++; var request_proxy = yield GLib.Bus.get_proxy ( GLib.BusType.SESSION, "org.freedesktop.portal.Desktop", @"/org/freedesktop/portal/desktop/request/$(sender_escaped)/$(handle_token)"); request_proxy.response.connect ( (response, results) => { Portal.destroy_request (handle_token); callback (response, results); }); if (requests == null) { requests = new GLib.HashTable (GLib.str_hash, GLib.str_equal); } requests.insert (handle_token, request_proxy); return handle_token; } public bool destroy_request (string handle_token) { return requests != null ? requests.remove (handle_token) : false; } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/000077500000000000000000000000001520625676500217205ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/capabilities.vala000066400000000000000000000142431520625676500252220ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni.Capabilities { private int get_env (string name) { switch (GLib.Environment.get_variable (name)?.ascii_down ()) { case "1": case "true": return 1; case "0": case "false": return 0; default: return -1; } } /** * Whether the host supports icon themes. Without it, we only can display app icon. */ internal bool have_icon_theme () { var env_value = get_env ("SNI_HAVE_ICON_THEME"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return true; case "kde": return true; case "cinnamon": return false; case "xfce": return true; case "lxqt": return true; // doesn't re-color symbolic icons though case "cosmic": return false; default: return false; } } /** * Whether the host supports PASSIVE status (when the timer stopped), * and the tray icon remain visible - just with lower priority. */ internal bool have_passive_status () { var env_value = get_env ("SNI_HAVE_PASSIVE_STATUS"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return false; case "kde": return true; case "cinnamon": return false; case "xfce": return false; case "lxqt": return false; case "cosmic": return false; default: return false; } } /** * Whether the host makes the icon blinking in NEEDS_ATTENTION status (when the timer is * paused or finished). */ internal bool have_attention_status () { var env_value = get_env ("SNI_HAVE_ATTENTION_STATUS"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return false; case "kde": return true; case "cinnamon": return false; case "xfce": return false; case "lxqt": return false; case "cosmic": return false; default: return false; } } /** * Whether the host prefers activation (clicking the icon brings the app to focus) * over displaying the context menu. */ internal bool have_activation () { var env_value = get_env ("SNI_HAVE_ACTIVATION"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return false; // via double-click case "kde": return true; case "cinnamon": return true; case "xfce": return true; case "lxqt": return true; // no activation token case "cosmic": return false; default: return true; } } /** * Whether the host displays tooltips. * * It's required for scroll-wheel gesture. Otherwise user would change countdown duration * without much of a visual cue. */ internal bool have_tooltips () { var env_value = get_env ("SNI_HAVE_TOOLTIPS"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return false; case "kde": return true; case "cinnamon": return false; // shows app title case "xfce": return false; // shows app title case "lxqt": return false; // no description case "cosmic": return false; default: return false; } } /** * Whether the desktop prefers having icons in their menus, * and the host can handle custom icon themes in the menu. */ internal bool have_menu_icons () { var env_value = get_env ("SNI_HAVE_MENU_ICONS"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return false; // broken icons case "kde": return true; case "cinnamon": return false; // broken layout case "xfce": return false; // broken layout case "lxqt": return false; case "cosmic": return false; default: return false; } } /** * Whether the host displays radios right next to the label * or does it *break* the menu layout. * * When `false`, we use custom icons to display radio / checkmark. */ internal bool have_toggles () { if (!have_menu_icons ()) { return true; } var env_value = get_env ("SNI_HAVE_TOGGLES"); if (env_value >= 0) { return (bool) env_value; } switch (Ft.get_desktop_name ()) { case "gnome": return true; case "kde": return false; // broken layout case "cinnamon": return true; case "xfce": return true; case "lxqt": return true; case "cosmic": return true; default: return true; } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/dbus-services.vala000066400000000000000000000614311520625676500253500ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { private inline GLib.Variant empty_vardict () { return new GLib.Variant.array (new GLib.VariantType ("{sv}"), {}); } private GLib.Variant serialize_pixmaps (Pixmap[] pixmaps) { GLib.Variant[] serialized_pixmaps = {}; foreach (var pixmap in pixmaps) { serialized_pixmaps += pixmap.to_variant (); } return new GLib.Variant.array (new GLib.VariantType ("(iiay)"), serialized_pixmaps); } /** * Build response for `GetLayout` method. * * The result value is recursive - it contains same structure for menu item children. */ private GLib.Variant serialize_layout (Sni.MenuItem menu_item, Sni.MenuItemProperty[] properties, int max_depth = -1) { var serialized_properties = menu_item.serialize_properties ( filter_properties (menu_item, properties)); var serialized_children = new GLib.Variant[0]; if (max_depth != 0) { var child_max_depth = max_depth > 0 ? max_depth - 1 : -1; menu_item.@foreach ( (child) => { serialized_children += new GLib.Variant.variant ( serialize_layout (child, properties, child_max_depth)); }); } return new GLib.Variant.tuple ({ new GLib.Variant.int32 ((int32) menu_item.id), serialized_properties ?? empty_vardict (), new GLib.Variant.array (GLib.VariantType.VARIANT, serialized_children), }); } /** * Filter-out properties with default values, unless the property has been modified. */ private Sni.MenuItemProperty[] filter_properties (Sni.MenuItem menu_item, Sni.MenuItemProperty[] properties) { var filtered_properties = new Sni.MenuItemProperty[0]; foreach (var property in properties) { if (!menu_item.has_modified_property (property) && menu_item.has_default_property (property)) { continue; } filtered_properties += property; } return filtered_properties; } private static Sni.MenuItemProperty[] unpack_properties (uint properties_mask) { var properties = new Sni.MenuItemProperty[0]; var index = 0U; while (properties_mask != 0U) { if ((properties_mask & 1U) != 0U) { properties += (Sni.MenuItemProperty) index; } properties_mask = properties_mask >> 1; index++; } return properties; } /** * A D-Bus service for exporting an tray icon. * * We try to keep things modern by not using pixmaps. Not all features are handled the same * way by various desktops. * * Note that unlike standard D-Bus services, the authors of the spec instead of using * `PropertiesChanged` signal invented custom signals (`New*`) for notifying about changes. * So, we don't emit `PropertiesChanged` as it's redundant in this case. * * https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/StatusNotifierItem/ * https://api.kde.org/kstatusnotifieritem.html */ [DBus (name = "org.kde.StatusNotifierItem")] private class StatusNotifierItemDBusService : GLib.Object { public string id { owned get { return Config.APPLICATION_ID; } } public string category { owned get { return "ApplicationStatus"; } } public bool item_is_menu { get { return this._item_is_menu; } } public GLib.ObjectPath menu { owned get { return this._menu; } } public string title { owned get { return this._title; } } public string status { owned get { return this._status.to_string (); } } public string icon_theme_path { // not in spec owned get { return this._icon_theme_path; } } public string icon_name { owned get { return this._icon_name; } } [DBus (signature = "a(iiay)")] public GLib.Variant icon_pixmap { owned get { return serialize_pixmaps ({}); } } public string overlay_icon_name { owned get { return this._overlay_icon_name; } } [DBus (signature = "a(iiay)")] public GLib.Variant overlay_icon_pixmap { owned get { return serialize_pixmaps ({}); } } public string attention_icon_name { owned get { return this.icon_name; } } [DBus (signature = "a(iiay)")] public GLib.Variant attention_icon_pixmap { owned get { return this.icon_pixmap; } } public string attention_movie_name { owned get { return ""; } } [DBus (name = "ToolTip", signature = "(sa(iiay)ss)")] public GLib.Variant tooltip { owned get { return this._tooltip.to_variant (); } } public int32 window_id { get { return 0; } } private Sni.IndicatorStatus _status = Sni.IndicatorStatus.ACTIVE; private Sni.Tooltip _tooltip; private GLib.ObjectPath _menu; private bool _item_is_menu; private string _title; private string _icon_theme_path; private string _icon_name; private string _overlay_icon_name; internal string activation_token; construct { this._item_is_menu = !Sni.Capabilities.have_activation (); this._title = GLib.Environment.get_application_name (); this._icon_name = @"$(Config.APPLICATION_ID)-symbolic"; this._overlay_icon_name = ""; this._tooltip = Sni.Tooltip (); this.activation_token = ""; } public StatusNotifierItemDBusService (string menu_object_path, string icon_theme_path = "") { this._menu = new GLib.ObjectPath (menu_object_path); this._icon_theme_path = icon_theme_path; } public void context_menu (int32 x, int32 y) throws GLib.DBusError, GLib.IOError { } public void provide_xdg_activation_token (string token) throws GLib.DBusError, GLib.IOError { this.activation_token = token ?? ""; this.received_activation_token (this.activation_token); } public void activate (int32 x, int32 y) throws GLib.DBusError, GLib.IOError { this.activated (this.activation_token); } public void secondary_activate (int32 x, int32 y) throws GLib.DBusError, GLib.IOError { this.secondary_activated (); } public void scroll (int32 delta, string orientation) throws GLib.DBusError, GLib.IOError { if (delta != 0 && orientation != null && orientation.ascii_down () == "vertical") { this.scrolled (delta); } } public signal void new_title (); public signal void new_icon (); public signal void new_attention_icon (); public signal void new_status (); public signal void new_menu (); [DBus (name = "NewToolTip")] public signal void new_tooltip (); /* * Internal API */ internal void set_title_internal (string value, bool emit_signal = true) { if (this._title == value) { return; } this._title = value; if (emit_signal) { this.new_title (); } } internal void set_icon_name_internal (string value, bool emit_signal = true) { if (this._icon_name == value) { return; } this._icon_name = value; if (emit_signal) { this.new_icon (); if (this._status == Sni.IndicatorStatus.NEEDS_ATTENTION) { this.new_attention_icon (); } } } internal Sni.IndicatorStatus get_status_internal () { return this._status; } internal void set_status_internal (Sni.IndicatorStatus value, bool emit_signal = true) { if (this._status == value) { return; } this._status = value; if (emit_signal) { if (value == Sni.IndicatorStatus.NEEDS_ATTENTION) { this.new_attention_icon (); } this.new_status (); } } internal Sni.Tooltip get_tooltip_internal () { return this._tooltip; } internal void set_tooltip_internal (Sni.Tooltip value, bool emit_signal = true) { if (this._tooltip.equals (value)) { return; } this._tooltip = value; if (emit_signal) { this.new_tooltip (); } } internal void emit_new_menu () { this.new_menu (); } [DBus (visible = false)] public signal void received_activation_token (string token); [DBus (visible = false)] public signal void activated (string token); [DBus (visible = false)] public signal void secondary_activated (); [DBus (visible = false)] public signal void scrolled (int delta); } /** * A D-Bus service for exporting an indicator menu. * * Menu structure should be static. * * https://github.com/gnustep/libs-dbuskit/blob/master/Bundles/DBusMenu/com.canonical.dbusmenu.xml */ [DBus (name = "com.canonical.dbusmenu")] private class DBusMenuService : GLib.Object { private const int32 ROOT_ID = 0; public uint32 version { get { return 4U; } } public string status { owned get { return "normal"; } } public string[] icon_theme_path { owned get { return {this._icon_theme_path}; } } public string text_direction { owned get { return Gtk.Widget.get_default_direction () == Gtk.TextDirection.RTL ? "rtl" : "ltr"; } } private string _icon_theme_path = null; private Sni.IndicatorActionGroup? action_group = null; private Sni.MenuItem? root = null; private uint32 revision = 1U; private GLib.HashTable menu_items_by_id; private GLib.HashTable menu_items_by_name; private uint layout_updated_idle_id = 0; private uint items_properties_updated_idle_id = 0; internal string activation_token = ""; public DBusMenuService (Sni.MenuItem root, Sni.IndicatorActionGroup action_group, string icon_theme_path) { this.root = root; this.action_group = action_group; this._icon_theme_path = icon_theme_path; this.menu_items_by_id = new GLib.HashTable (GLib.direct_hash, GLib.direct_equal); this.menu_items_by_name = new GLib.HashTable (GLib.str_hash, GLib.str_equal); this.root.traverse ( (menu_item) => { this.menu_items_by_id.insert (menu_item.id, menu_item); if (menu_item.name != "" && menu_item.name != null) { this.menu_items_by_name.insert (menu_item.name, menu_item); } menu_item.child_added.connect (this.on_child_added); menu_item.child_added.connect (this.on_child_removed); menu_item.changed.connect (this.on_changed); }); } private inline unowned Sni.MenuItem? lookup_by_id (int32 id) { return id >= 0 ? this.menu_items_by_id.lookup ((uint) id) : null; } private inline unowned Sni.MenuItem? lookup_by_name (string name) { return this.menu_items_by_name.lookup (name); } internal unowned Sni.MenuItem? lookup_menu_item (string name) { return this.lookup_by_name (name); } internal void emit_layout_updated () { if (this.root == null) { return; } if (this.layout_updated_idle_id != 0) { GLib.Source.remove (this.layout_updated_idle_id); this.layout_updated_idle_id = 0; } uint[] changed_ids = {}; this.root.traverse ( (menu_item) => { if (menu_item.dirty_layout) { changed_ids += menu_item.id; menu_item.dirty_layout = false; } }); if (changed_ids.length > 0) { this.revision++; this.layout_updated (this.revision, (int32) this.root.id); // TODO: find common ancestor } } private void queue_layout_updated () { if (this.layout_updated_idle_id != 0) { return; } this.layout_updated_idle_id = GLib.Idle.add ( () => { this.layout_updated_idle_id = 0; if (this.root != null) { this.layout_updated (this.revision, (int32) this.root.id); // TODO: try to pass parent_id of the changed submenu - not the rooot } return GLib.Source.REMOVE; }); } private void queue_items_properties_updated () { if (this.layout_updated_idle_id != 0) { return; } } private void emit_items_properties_updated () { if (this.root == null) { return; } if (this.items_properties_updated_idle_id != 0) { GLib.Source.remove (this.items_properties_updated_idle_id); this.items_properties_updated_idle_id = 0; } var builder = new GLib.VariantBuilder (new GLib.VariantType ("a(ia{sv})")); var changed = false; this.root.traverse ( (menu_item) => { if (menu_item.dirty_properties != 0U) { builder.add_value ( new GLib.Variant.tuple ({ new GLib.Variant.int32 ((int32) menu_item.id), menu_item.serialize_properties ( unpack_properties (menu_item.dirty_properties)) }) ); menu_item.dirty_properties = 0U; changed = true; } }); if (changed) { this.items_properties_updated ( builder.end (), new GLib.Variant.array (new GLib.VariantType ("(ias)"), {})); } } internal void emit_updates () { this.emit_layout_updated (); this.emit_items_properties_updated (); } private void on_child_added (Sni.MenuItem menu_item, Sni.MenuItem child) { menu_item.mark_dirty_layout (); menu_item.mark_dirty_property (Sni.MenuItemProperty.CHILDREN_DISPLAY); this.queue_layout_updated (); child.child_added.connect (this.on_child_added); child.child_added.connect (this.on_child_removed); } private void on_child_removed (Sni.MenuItem menu_item, Sni.MenuItem child) { menu_item.mark_dirty_layout (); menu_item.mark_dirty_property (Sni.MenuItemProperty.CHILDREN_DISPLAY); this.queue_layout_updated (); child.child_added.disconnect (this.on_child_added); child.child_removed.disconnect (this.on_child_removed); } private void on_changed (Sni.MenuItem menu_item, Sni.MenuItemProperty property) { menu_item.mark_dirty_property (property); this.queue_items_properties_updated (); } private void handle_event (int32 id, string event_name, GLib.Variant data, uint32 timestamp) throws GLib.DBusError { if (event_name != "clicked") { return; } unowned var menu_item = this.lookup_by_id (id); if (menu_item == null) { throw new GLib.DBusError.INVALID_ARGS ("Unknown id"); } this.action_group.activate_action_full (menu_item.action_name, menu_item.action_target, build_platform_data (this.activation_token)); } /* * Public D-Bus API */ public void get_layout (int32 parent_id, int32 recursion_depth, string[] property_names, out uint32 revision, [DBus (signature = "(ia{sv}av)")] out GLib.Variant layout) throws GLib.DBusError, GLib.IOError { unowned var parent = this.lookup_by_id (parent_id); if (parent == null) { throw new GLib.DBusError.INVALID_ARGS ("Unknown parent id"); } revision = this.revision; layout = serialize_layout (parent, Sni.MenuItemProperty.from_strv (property_names), recursion_depth); } [DBus (name = "GetProperty", signature = "v")] public GLib.Variant get_property_ (int32 id, string name) throws GLib.DBusError, GLib.IOError { unowned var menu_item = this.lookup_by_id (id); if (menu_item == null) { throw new GLib.DBusError.INVALID_ARGS ("Unknown menu item id"); } var property = Sni.MenuItemProperty.from_string (name); var value = menu_item.serialize_property (property); if (value == null) { throw new GLib.DBusError.INVALID_ARGS ("Unknown property"); } return new GLib.Variant.variant (value); } [DBus (signature = "a(ia{sv})")] public GLib.Variant get_group_properties (int32[] ids, string[] property_names) throws GLib.DBusError, GLib.IOError { var properties = Sni.MenuItemProperty.from_strv (property_names); var builder = new GLib.VariantBuilder (new GLib.VariantType ("a(ia{sv})")); foreach (var id in ids) { var menu_item = this.lookup_by_id (id); if (menu_item == null) { continue; } var data = menu_item.serialize_properties ( filter_properties (menu_item, properties)); if (data == null) { continue; } builder.add_value ( new GLib.Variant.tuple ({ new GLib.Variant.int32 ((int32) menu_item.id), data })); } return builder.end (); } public void event (int32 id, string event_id, GLib.Variant data, uint32 timestamp) throws GLib.DBusError, GLib.IOError { this.handle_event (id, event_id, data, timestamp); } public int32[] event_group ([DBus (signature = "a(isvu)")] GLib.Variant events) throws GLib.DBusError, GLib.IOError { int32 id; string event_id; GLib.Variant data; uint32 timestamp; var n = events.n_children (); for (var index = 0; index < n; index++) { var event = events.get_child_value (index); event.get ("(isvu)", out id, out event_id, out data, out timestamp); this.handle_event (id, event_id, data, timestamp); } return {}; } public bool about_to_show (int32 id) throws GLib.DBusError, GLib.IOError { unowned var menu_item = this.lookup_by_id (id); return menu_item != null ? menu_item.needs_update () : false; } public void about_to_show_group (int32[] ids, out int32[] updates_needed, out int32[] id_errors) throws GLib.DBusError, GLib.IOError { updates_needed = new int32[ids.length]; id_errors = new int32[ids.length]; for (var index = 0; index < ids.length; index < index++) { unowned var menu_item = this.lookup_by_id (ids[index]); updates_needed[index] = (int32)(menu_item != null && menu_item.needs_update ()); id_errors[index] = (int32)(menu_item == null); } } /** * Triggered when there are property updates across many items. */ public signal void items_properties_updated ( [DBus (signature = "a(ia{sv})")] GLib.Variant updated_props, [DBus (signature = "a(ias)")] GLib.Variant removed_props); /** * Triggered by app, notify client to update the menu. * * Passing current `revision` as the client may already have the latest update. */ public signal void layout_updated (uint32 revision, int32 parent_id); /** * Triggered by app, requesting to open the menu. */ public signal void item_activation_requested (int32 id, uint32 timestamp); public override void dispose () { if (this.layout_updated_idle_id != 0) { GLib.Source.remove (this.layout_updated_idle_id); this.layout_updated_idle_id = 0; } if (this.items_properties_updated_idle_id != 0) { GLib.Source.remove (this.items_properties_updated_idle_id); this.items_properties_updated_idle_id = 0; } this.menu_items_by_id = null; this.menu_items_by_name = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/000077500000000000000000000000001520625676500230335ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/000077500000000000000000000000001520625676500236205ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/000077500000000000000000000000001520625676500251435ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-000-symbolic.svg000066400000000000000000000012011520625676500330300ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-005-symbolic.svg000066400000000000000000000014431520625676500330450ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-010-symbolic.svg000066400000000000000000000021111520625676500330320ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-015-symbolic.svg000066400000000000000000000021031520625676500330400ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-020-symbolic.svg000066400000000000000000000020741520625676500330430ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-025-symbolic.svg000066400000000000000000000020661520625676500330510ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-030-symbolic.svg000066400000000000000000000016731520625676500330500ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-035-symbolic.svg000066400000000000000000000016261520625676500330530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-040-symbolic.svg000066400000000000000000000016241520625676500330450ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-045-symbolic.svg000066400000000000000000000016751520625676500330600ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-050-symbolic.svg000066400000000000000000000015721520625676500330500ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-055-symbolic.svg000066400000000000000000000020221520625676500330440ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-060-symbolic.svg000066400000000000000000000017411520625676500330470ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-065-symbolic.svg000066400000000000000000000021001520625676500330420ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-070-symbolic.svg000066400000000000000000000016301520625676500330450ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-075-symbolic.svg000066400000000000000000000016541520625676500330600ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-080-symbolic.svg000066400000000000000000000016341520625676500330520ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-085-symbolic.svg000066400000000000000000000016251520625676500330570ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-090-symbolic.svg000066400000000000000000000020751520625676500330530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-095-symbolic.svg000066400000000000000000000021061520625676500330530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-break-100-symbolic.svg000066400000000000000000000012231520625676500330350ustar00rootroot00000000000000 indicator-break-paused-symbolic.svg000066400000000000000000000007451520625676500337470ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-000-symbolic.svg000066400000000000000000000011461520625676500335330ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-005-symbolic.svg000066400000000000000000000014101520625676500335320ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-010-symbolic.svg000066400000000000000000000020361520625676500335330ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-015-symbolic.svg000066400000000000000000000020301520625676500335320ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-020-symbolic.svg000066400000000000000000000020211520625676500335260ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-025-symbolic.svg000066400000000000000000000020131520625676500335340ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-030-symbolic.svg000066400000000000000000000016201520625676500335330ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-035-symbolic.svg000066400000000000000000000015531520625676500335450ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-040-symbolic.svg000066400000000000000000000015511520625676500335370ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-045-symbolic.svg000066400000000000000000000016221520625676500335430ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-050-symbolic.svg000066400000000000000000000015171520625676500335420ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-055-symbolic.svg000066400000000000000000000017471520625676500335540ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-060-symbolic.svg000066400000000000000000000016661520625676500335500ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-065-symbolic.svg000066400000000000000000000020251520625676500335430ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-070-symbolic.svg000066400000000000000000000015551520625676500335460ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-075-symbolic.svg000066400000000000000000000016011520625676500335430ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-080-symbolic.svg000066400000000000000000000015611520625676500335440ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-085-symbolic.svg000066400000000000000000000015521520625676500335510ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-090-symbolic.svg000066400000000000000000000020221520625676500335360ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-095-symbolic.svg000066400000000000000000000020331520625676500335450ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-100-symbolic.svg000066400000000000000000000011701520625676500335310ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status indicator-pomodoro-paused-symbolic.svg000066400000000000000000000006721520625676500345200ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/16x16/status/indicator-stopped-symbolic.svg000066400000000000000000000011701520625676500331320ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/000077500000000000000000000000001520625676500236125ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/000077500000000000000000000000001520625676500251355ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-000-symbolic.svg000066400000000000000000000011141520625676500330250ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-005-symbolic.svg000066400000000000000000000014131520625676500330340ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-010-symbolic.svg000066400000000000000000000020771520625676500330370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-015-symbolic.svg000066400000000000000000000020731520625676500330400ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-020-symbolic.svg000066400000000000000000000021021520625676500330250ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-025-symbolic.svg000066400000000000000000000020531520625676500330370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-030-symbolic.svg000066400000000000000000000016251520625676500330370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-035-symbolic.svg000066400000000000000000000016351520625676500330450ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-040-symbolic.svg000066400000000000000000000016761520625676500330460ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-045-symbolic.svg000066400000000000000000000017011520625676500330400ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-050-symbolic.svg000066400000000000000000000015731520625676500330430ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-055-symbolic.svg000066400000000000000000000017701520625676500330470ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-060-symbolic.svg000066400000000000000000000020241520625676500330340ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-065-symbolic.svg000066400000000000000000000016641520625676500330520ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-070-symbolic.svg000066400000000000000000000016411520625676500330410ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-075-symbolic.svg000066400000000000000000000016041520625676500330450ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-080-symbolic.svg000066400000000000000000000016341520625676500330440ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-085-symbolic.svg000066400000000000000000000016331520625676500330500ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-090-symbolic.svg000066400000000000000000000021141520625676500330370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-095-symbolic.svg000066400000000000000000000021161520625676500330460ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-break-100-symbolic.svg000066400000000000000000000011361520625676500330320ustar00rootroot00000000000000 indicator-break-paused-symbolic.svg000066400000000000000000000007621520625676500337400ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-000-symbolic.svg000066400000000000000000000010611520625676500335210ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-005-symbolic.svg000066400000000000000000000013601520625676500335300ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-010-symbolic.svg000066400000000000000000000020241520625676500335220ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-015-symbolic.svg000066400000000000000000000020201520625676500335230ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-020-symbolic.svg000066400000000000000000000020271520625676500335260ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-025-symbolic.svg000066400000000000000000000020001520625676500335220ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-030-symbolic.svg000066400000000000000000000015521520625676500335310ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-035-symbolic.svg000066400000000000000000000015621520625676500335370ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-040-symbolic.svg000066400000000000000000000016231520625676500335310ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-045-symbolic.svg000066400000000000000000000016261520625676500335410ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-050-symbolic.svg000066400000000000000000000015201520625676500335260ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-055-symbolic.svg000066400000000000000000000017151520625676500335410ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-060-symbolic.svg000066400000000000000000000017511520625676500335350ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-065-symbolic.svg000066400000000000000000000016111520625676500335350ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-070-symbolic.svg000066400000000000000000000015661520625676500335420ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-075-symbolic.svg000066400000000000000000000015311520625676500335370ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-080-symbolic.svg000066400000000000000000000015611520625676500335360ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-085-symbolic.svg000066400000000000000000000015601520625676500335420ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-090-symbolic.svg000066400000000000000000000020411520625676500335310ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-095-symbolic.svg000066400000000000000000000020431520625676500335400ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-100-symbolic.svg000066400000000000000000000011031520625676500335170ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status indicator-pomodoro-paused-symbolic.svg000066400000000000000000000007071520625676500345110ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/22x22/status/indicator-stopped-symbolic.svg000066400000000000000000000011031520625676500331200ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/index.theme000066400000000000000000000006471520625676500251750ustar00rootroot00000000000000[Icon Theme] Name=Hicolor Hidden=true Directories=16x16/status,16x16@2/status,22x22/status,22x22@2/status,scalable/actions [16x16/status] Context=Status Type=Fixed Size=16 [16x16@2/status] Context=Status Type=Fixed Size=16 Scale=2 [22x22/status] Context=Status Type=Fixed Size=22 [22x22@2/status] Context=Status Type=Fixed Size=22 Scale=2 [scalable/actions] Context=Actions Type=Scalable MinSize=8 Size=16 MaxSize=512 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/000077500000000000000000000000001520625676500246015ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/000077500000000000000000000000001520625676500262415ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/ornament-check-symbolic.svg000066400000000000000000000013571520625676500335050ustar00rootroot00000000000000 ornament-dot-checked-symbolic.svg000066400000000000000000000005761520625676500345250ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions ornament-dot-unchecked-symbolic.svg000066400000000000000000000001741520625676500350620ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-pause-symbolic.svg000066400000000000000000000010141520625676500330300ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-reset-symbolic.svg000066400000000000000000000014111520625676500330360ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-skip-symbolic-rtl.svg000066400000000000000000000012751520625676500334710ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-skip-symbolic.svg000066400000000000000000000013001520625676500326570ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-start-symbolic.svg000066400000000000000000000011221520625676500330500ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/icons/scalable/actions/timer-stop-symbolic.svg000066400000000000000000000006621520625676500327100ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/plugins/sni/indicator-action-group.vala000066400000000000000000000201431520625676500271460ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { public class IndicatorActionGroup : GLib.Object, GLib.ActionGroup, GLib.RemoteActionGroup { private GLib.ActionGroup? application_action_group = null; private GLib.ActionGroup? timer_action_group = null; private GLib.ActionGroup? session_manager_action_group = null; private GLib.HashTable? actions; construct { this.application_action_group = (GLib.ActionGroup) GLib.Application.get_default (); this.timer_action_group = new Ft.TimerActionGroup (); this.session_manager_action_group = new Ft.SessionManagerActionGroup (); this.actions = new GLib.HashTable (GLib.str_hash, GLib.str_equal); var quit_action = new GLib.SimpleAction ("quit", null); quit_action.activate.connect (this.activate_quit); this.add_action (quit_action); } private inline void split_name (string action_name, out string prefix, out string name) { var action_name_parts = action_name.split (".", 2); if (action_name_parts.length < 2) { prefix = "indicator"; name = action_name; } else { prefix = action_name_parts[0]; name = action_name_parts[1]; } } private unowned GLib.ActionGroup? lookup_group (string prefix) { switch (prefix) { case "indicator": return this; case "app": return this.application_action_group; case "timer": return this.timer_action_group; case "session-manager": return this.session_manager_action_group; default: return null; } } /* * Action Group */ public void activate_action (string action_name, GLib.Variant? parameter) { string prefix; string action_name_; this.split_name (action_name, out prefix, out action_name_); unowned var action_group = this.lookup_group (prefix); if (action_group == this) { unowned var action = this.actions.lookup (action_name_); action?.activate (parameter); } else if (action_group != null) { action_group.activate_action (action_name_, parameter); } } public void change_action_state (string action_name, GLib.Variant value) { string prefix; string action_name_; this.split_name (action_name, out prefix, out action_name_); unowned var action_group = this.lookup_group (prefix); if (action_group == this) { unowned var action = this.actions.lookup (action_name_); action?.change_state (value); } else if (action_group != null) { action_group.change_action_state (action_name_, value); } } /* * ActionMap */ public void add_action (GLib.Action action) { string prefix; string action_name; this.split_name (action.name, out prefix, out action_name); unowned var action_group = this.lookup_group (prefix); if (action_group == this) { this.actions.insert (action_name, action); } else if (action_group is GLib.ActionMap) { action_group.add_action (action); } else { GLib.warning ("Unable to add action '%s'", action.name); } } public string[] list_actions () { var action_names = new string[0]; foreach (var action_name in this.actions.get_keys_as_array ()) { action_names += @"indicator.$(action_name)"; } foreach (var action_name in this.application_action_group.list_actions ()) { action_names += @"app.$(action_name)"; } foreach (var action_name in this.timer_action_group.list_actions ()) { action_names += @"timer.$(action_name)"; } foreach (var action_name in this.session_manager_action_group.list_actions ()) { action_names += @"session-manager.$(action_name)"; } return action_names; } /* * RemoteActionGroup */ private static void activate_action_remote (string action_name, GLib.Variant? parameter, GLib.Variant platform_data, int timeout = -1, GLib.Cancellable? cancellable = null) { var application = GLib.Application.get_default (); var connection = application?.get_dbus_connection (); if (connection == null) { return; } var parameters = new GLib.VariantBuilder (new GLib.VariantType ("av")); if (parameter != null) { parameters.add_value (new GLib.Variant.variant (parameter)); } connection.call.begin ( application.get_application_id (), application.get_dbus_object_path (), "org.freedesktop.Application", "ActivateAction", new GLib.Variant.tuple ({ new GLib.Variant.string (action_name), parameters.end (), platform_data, }), null, GLib.DBusCallFlags.NONE, timeout, cancellable, (obj, res) => { try { connection.call.end (res); } catch (GLib.Error error) { GLib.warning ("Failed to activate action '%s': %s", action_name, error.message); } }); } public void activate_action_full (string action_name, GLib.Variant? parameter, GLib.Variant platform_data) { var action_name_parts = action_name.split (".", 2); if (action_name_parts.length == 1) { activate_action_remote (action_name, parameter, platform_data); return; } if (action_name_parts[0] == "app") { activate_action_remote (action_name_parts[1], parameter, platform_data); return; } this.activate_action (action_name, parameter); } public void change_action_state_full (string action_name, GLib.Variant value, GLib.Variant platform_data) { this.change_action_state (action_name, value); } /* * Indicator actions */ private void activate_quit (GLib.SimpleAction action, GLib.Variant? parameter) { // Delay, so that we return a reply to the D-Bus client. GLib.Idle.add (() => { this.activate_action ("app.quit", null); return GLib.Source.REMOVE; }); } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/indicator-provider.vala000066400000000000000000001321431520625676500263750ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { private GLib.Variant build_platform_data (string activation_token) { var platform_data = new GLib.VariantBuilder (new GLib.VariantType ("a{sv}")); if (activation_token != null && activation_token != "") { platform_data.add ("{sv}", "activation-token", new GLib.Variant.string (activation_token)); } return platform_data.end (); } private inline string format_remaining_time (int64 remaining) { var seconds_uint = (uint) Ft.round_seconds (Ft.Timestamp.to_seconds (remaining)); return _("%s remaining").printf (Ft.format_time (seconds_uint)); } private string format_tooltip_title (Ft.State state, uint cycle_number, uint cycle_count, bool is_finished) { if (is_finished) { return _("Finished!"); } if (cycle_number > 0 && cycle_count > 1 && ( state == Ft.State.POMODORO || state == Ft.State.SHORT_BREAK)) { var state_label = state.get_label (); var cycle_label = _("%u of %u").printf (cycle_number, cycle_count); return @"$(state_label) ($(cycle_label))"; } return state.get_label (); } private string normalize_separators (string resource_path) { return resource_path.replace ("/", GLib.Path.DIR_SEPARATOR_S); } private errordomain IndicatorError { CONNECTION, HOST, INDICATOR, INDICATOR_MENU } /** * Class for exporting and updating the indicator and its context menu */ private class IndicatorController { private const string BUS_NAME = "%s.StatusNotifierItem"; private const string OBJECT_PATH = "/StatusNotifierItem"; private const string MENU_OBJECT_PATH = "%s/StatusNotifierMenu"; private const uint ICON_STEPS = 20U; private const string[] ICONS = { "16x16/status/indicator-break-000-symbolic.svg", "16x16/status/indicator-break-005-symbolic.svg", "16x16/status/indicator-break-010-symbolic.svg", "16x16/status/indicator-break-015-symbolic.svg", "16x16/status/indicator-break-020-symbolic.svg", "16x16/status/indicator-break-025-symbolic.svg", "16x16/status/indicator-break-030-symbolic.svg", "16x16/status/indicator-break-035-symbolic.svg", "16x16/status/indicator-break-040-symbolic.svg", "16x16/status/indicator-break-045-symbolic.svg", "16x16/status/indicator-break-050-symbolic.svg", "16x16/status/indicator-break-055-symbolic.svg", "16x16/status/indicator-break-060-symbolic.svg", "16x16/status/indicator-break-065-symbolic.svg", "16x16/status/indicator-break-070-symbolic.svg", "16x16/status/indicator-break-075-symbolic.svg", "16x16/status/indicator-break-080-symbolic.svg", "16x16/status/indicator-break-085-symbolic.svg", "16x16/status/indicator-break-090-symbolic.svg", "16x16/status/indicator-break-095-symbolic.svg", "16x16/status/indicator-break-100-symbolic.svg", "16x16/status/indicator-break-paused-symbolic.svg", "16x16/status/indicator-pomodoro-000-symbolic.svg", "16x16/status/indicator-pomodoro-005-symbolic.svg", "16x16/status/indicator-pomodoro-010-symbolic.svg", "16x16/status/indicator-pomodoro-015-symbolic.svg", "16x16/status/indicator-pomodoro-020-symbolic.svg", "16x16/status/indicator-pomodoro-025-symbolic.svg", "16x16/status/indicator-pomodoro-030-symbolic.svg", "16x16/status/indicator-pomodoro-035-symbolic.svg", "16x16/status/indicator-pomodoro-040-symbolic.svg", "16x16/status/indicator-pomodoro-045-symbolic.svg", "16x16/status/indicator-pomodoro-050-symbolic.svg", "16x16/status/indicator-pomodoro-055-symbolic.svg", "16x16/status/indicator-pomodoro-060-symbolic.svg", "16x16/status/indicator-pomodoro-065-symbolic.svg", "16x16/status/indicator-pomodoro-070-symbolic.svg", "16x16/status/indicator-pomodoro-075-symbolic.svg", "16x16/status/indicator-pomodoro-080-symbolic.svg", "16x16/status/indicator-pomodoro-085-symbolic.svg", "16x16/status/indicator-pomodoro-090-symbolic.svg", "16x16/status/indicator-pomodoro-095-symbolic.svg", "16x16/status/indicator-pomodoro-100-symbolic.svg", "16x16/status/indicator-pomodoro-paused-symbolic.svg", "16x16/status/indicator-stopped-symbolic.svg", "22x22/status/indicator-break-000-symbolic.svg", "22x22/status/indicator-break-005-symbolic.svg", "22x22/status/indicator-break-010-symbolic.svg", "22x22/status/indicator-break-015-symbolic.svg", "22x22/status/indicator-break-020-symbolic.svg", "22x22/status/indicator-break-025-symbolic.svg", "22x22/status/indicator-break-030-symbolic.svg", "22x22/status/indicator-break-035-symbolic.svg", "22x22/status/indicator-break-040-symbolic.svg", "22x22/status/indicator-break-045-symbolic.svg", "22x22/status/indicator-break-050-symbolic.svg", "22x22/status/indicator-break-055-symbolic.svg", "22x22/status/indicator-break-060-symbolic.svg", "22x22/status/indicator-break-065-symbolic.svg", "22x22/status/indicator-break-070-symbolic.svg", "22x22/status/indicator-break-075-symbolic.svg", "22x22/status/indicator-break-080-symbolic.svg", "22x22/status/indicator-break-085-symbolic.svg", "22x22/status/indicator-break-090-symbolic.svg", "22x22/status/indicator-break-095-symbolic.svg", "22x22/status/indicator-break-100-symbolic.svg", "22x22/status/indicator-break-paused-symbolic.svg", "22x22/status/indicator-pomodoro-000-symbolic.svg", "22x22/status/indicator-pomodoro-005-symbolic.svg", "22x22/status/indicator-pomodoro-010-symbolic.svg", "22x22/status/indicator-pomodoro-015-symbolic.svg", "22x22/status/indicator-pomodoro-020-symbolic.svg", "22x22/status/indicator-pomodoro-025-symbolic.svg", "22x22/status/indicator-pomodoro-030-symbolic.svg", "22x22/status/indicator-pomodoro-035-symbolic.svg", "22x22/status/indicator-pomodoro-040-symbolic.svg", "22x22/status/indicator-pomodoro-045-symbolic.svg", "22x22/status/indicator-pomodoro-050-symbolic.svg", "22x22/status/indicator-pomodoro-055-symbolic.svg", "22x22/status/indicator-pomodoro-060-symbolic.svg", "22x22/status/indicator-pomodoro-065-symbolic.svg", "22x22/status/indicator-pomodoro-070-symbolic.svg", "22x22/status/indicator-pomodoro-075-symbolic.svg", "22x22/status/indicator-pomodoro-080-symbolic.svg", "22x22/status/indicator-pomodoro-085-symbolic.svg", "22x22/status/indicator-pomodoro-090-symbolic.svg", "22x22/status/indicator-pomodoro-095-symbolic.svg", "22x22/status/indicator-pomodoro-100-symbolic.svg", "22x22/status/indicator-pomodoro-paused-symbolic.svg", "22x22/status/indicator-stopped-symbolic.svg", "scalable/actions/ornament-check-symbolic.svg", "scalable/actions/ornament-dot-checked-symbolic.svg", "scalable/actions/ornament-dot-unchecked-symbolic.svg", "scalable/actions/timer-pause-symbolic.svg", "scalable/actions/timer-reset-symbolic.svg", "scalable/actions/timer-skip-symbolic.svg", "scalable/actions/timer-skip-symbolic-rtl.svg", "scalable/actions/timer-start-symbolic.svg", "scalable/actions/timer-stop-symbolic.svg", "index.theme", }; private const string[] SCALES = { "@2", }; private GLib.DBusConnection? connection = null; private Sni.StatusNotifierWatcher? watcher_proxy = null; private Ft.Timer? timer = null; private Ft.SessionManager? session_manager = null; private Ft.NotificationManager? notification_manager = null; private GLib.Settings? application_settings = null; private Sni.IndicatorActionGroup? action_group = null; private uint name_owner_id = 0; private Sni.StatusNotifierItemDBusService? service = null; private uint service_id = 0; private Sni.DBusMenuService? menu_service = null; private uint menu_service_id = 0; private unowned Sni.MenuItem? primary_item = null; private unowned Sni.MenuItem? secondary_item = null; private uint update_icon_timeout_id = 0U; private uint update_idle_id = 0U; private string? tooltip_title = null; private bool have_icon_theme; private bool have_passive_status; private bool have_attention_status; private bool have_activation; private bool have_menu_icons; private bool have_tooltips; private bool have_toggles; public IndicatorController (GLib.DBusConnection connection, Sni.StatusNotifierWatcher watcher_proxy) { this.connection = connection; this.watcher_proxy = watcher_proxy; this.timer = Ft.Timer.get_default (); this.session_manager = Ft.SessionManager.get_default (); this.notification_manager = new Ft.NotificationManager (); this.application_settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer"); this.action_group = new Sni.IndicatorActionGroup (); this.have_icon_theme = Sni.Capabilities.have_icon_theme (); this.have_passive_status = Sni.Capabilities.have_passive_status (); this.have_attention_status = Sni.Capabilities.have_attention_status (); this.have_activation = Sni.Capabilities.have_activation (); this.have_menu_icons = Sni.Capabilities.have_menu_icons (); this.have_tooltips = Sni.Capabilities.have_tooltips (); this.have_toggles = Sni.Capabilities.have_toggles (); } ~IndicatorController () { if (this.service_id != 0 || this.menu_service_id != 0 || this.name_owner_id != 0) { GLib.critical ("SNI D-Bus services not destroyed properly"); } this.service = null; this.menu_service = null; this.watcher_proxy = null; this.connection = null; this.action_group = null; this.timer = null; this.session_manager = null; this.application_settings = null; this.notification_manager = null; this.primary_item = null; this.secondary_item = null; } private Sni.MenuItem build_menu () { var is_rtl = Gtk.Widget.get_default_direction () == Gtk.TextDirection.RTL; var start_item = new Sni.MenuItem ("start", _("Start"), "timer-start-symbolic", "timer.start"); start_item.visible = false; var pause_item = new Sni.MenuItem ("pause", _("Pause"), "timer-pause-symbolic", "timer.pause"); pause_item.visible = false; var resume_item = new Sni.MenuItem ("resume", _("Resume"), "timer-start-symbolic", "timer.resume"); resume_item.visible = false; var advance_item = new Sni.MenuItem ("advance", "", "timer-start-symbolic", "session-manager.advance"); advance_item.visible = false; var stop_item = new Sni.MenuItem ("stop", _("Stop"), "timer-stop-symbolic", "timer.reset"); stop_item.visible = false; var skip_item = new Sni.MenuItem ("skip", _("Skip"), is_rtl ? "timer-skip-symbolic-rtl" : "timer-skip-symbolic", "session-manager.advance"); skip_item.visible = false; var reset_item = new Sni.MenuItem ("reset", _("Reset"), "timer-reset-symbolic", "session-manager.reset"); reset_item.visible = false; var pomodoro_item = new Sni.MenuItem ("pomodoro", _("Pomodoro"), "", "session-manager.state", new GLib.Variant.string ("pomodoro")); var short_break_item = new Sni.MenuItem ("short-break", _("Short Break"), "", "session-manager.state", new GLib.Variant.string ("short-break")); var long_break_item = new Sni.MenuItem ("long-break", _("Long Break"), "", "session-manager.state", new GLib.Variant.string ("long-break")); var break_item = new Sni.MenuItem ("break", _("Break"), "", "session-manager.state", new GLib.Variant.string ("break")); if (this.have_toggles) { pomodoro_item.toggle_type = Sni.MenuToggleType.RADIO; short_break_item.toggle_type = Sni.MenuToggleType.RADIO; long_break_item.toggle_type = Sni.MenuToggleType.RADIO; break_item.toggle_type = Sni.MenuToggleType.RADIO; } var root = new Sni.MenuItem.root (); root.append (start_item); root.append (pause_item); root.append (resume_item); root.append (advance_item); root.append (stop_item); root.append (skip_item); root.append (reset_item); root.append (new Sni.MenuItem.separator ("state-separator")); root.append (pomodoro_item); root.append (short_break_item); root.append (long_break_item); root.append (break_item); root.append (new Sni.MenuItem.separator ()); root.append (new Sni.MenuItem ("screen-overlay", _("Screen Overlay"), "", "app.screen-overlay")); if (this.have_activation) { root.append (new Sni.MenuItem ("preferences", _("Preferences"), "", "app.preferences")); root.append (new Sni.MenuItem ("stats", _("Stats"), "", "app.window", new GLib.Variant.string ("stats"))); } else { root.append (new Sni.MenuItem ("timer", _("Timer"), "", "app.window", new GLib.Variant.string ("timer"))); root.append (new Sni.MenuItem ("stats", _("Stats"), "", "app.window", new GLib.Variant.string ("stats"))); root.append (new Sni.MenuItem ("preferences", _("Preferences"), "", "app.preferences")); } root.append (new Sni.MenuItem.separator ()); root.append (new Sni.MenuItem ("quit", _("Quit"), "", "indicator.quit")); if (!this.have_menu_icons) { root.traverse (menu_item => { menu_item.icon_name = ""; }); } return root; } private void get_cycle_number_count (out uint cycle_number, out uint cycle_count) { var tmp_cycle_number = 0U; var tmp_cycle_count = 0U; this.session_manager.current_session?.get_cycles ().@foreach ( (cycle) => { var cycle_status = cycle.get_status (); if (cycle_status == Ft.TimeBlockStatus.UNCOMPLETED || cycle.get_weight () <= 0.0) { return; } if (cycle_status == Ft.TimeBlockStatus.COMPLETED || cycle_status == Ft.TimeBlockStatus.IN_PROGRESS) { tmp_cycle_number++; } tmp_cycle_count++; }); cycle_number = tmp_cycle_number; cycle_count = tmp_cycle_count; } private void set_toggle_state (Sni.MenuItem menu_item, bool value) { if (this.have_toggles) { menu_item.toggle_state = value; } else { menu_item.icon_name = value ? "ornament-dot-checked-symbolic" : "ornament-dot-unchecked-symbolic"; } } private void update_menu_items () { var state = this.session_manager.current_state; var is_break = state.is_break (); var is_started = this.timer.is_started (); var is_paused = this.timer.is_paused (); var is_finished = this.timer.is_finished (); // Timer string primary_name = null; string secondary_name = null; if (!is_started) { primary_name = "start"; secondary_name = this.session_manager.can_reset () ? "reset" : null; } else { if (is_paused) { primary_name = "resume"; secondary_name = "stop"; } else if (is_finished) { primary_name = "advance"; secondary_name = "stop"; } else { primary_name = "pause"; secondary_name = "skip"; } } if (this.primary_item?.name != primary_name) { if (this.primary_item != null) { this.primary_item.visible = false; } if (primary_name != null) { this.primary_item = this.menu_service.lookup_menu_item (primary_name); this.primary_item.visible = true; } if (primary_name == "advance") { this.primary_item.label = is_break ? _("Start Pomodoro") : _("Take Break"); } } if (this.secondary_item?.name != secondary_name) { if (this.secondary_item != null) { this.secondary_item.visible = false; } if (secondary_name != null) { this.secondary_item = this.menu_service.lookup_menu_item (secondary_name); this.secondary_item.visible = true; } } // State var state_separator = this.menu_service.lookup_menu_item ("state-separator"); state_separator.visible = is_started; var pomodoro_item = this.menu_service.lookup_menu_item ("pomodoro"); pomodoro_item.visible = is_started; this.set_toggle_state (pomodoro_item, state == Ft.State.POMODORO); var break_item = this.menu_service.lookup_menu_item ("break"); break_item.visible = is_started && this.session_manager.has_uniform_breaks; this.set_toggle_state (break_item, state == Ft.State.BREAK); var short_break_item = this.menu_service.lookup_menu_item ("short-break"); short_break_item.visible = is_started && !break_item.visible; this.set_toggle_state (short_break_item, state == Ft.State.SHORT_BREAK); var long_break_item = this.menu_service.lookup_menu_item ("long-break"); long_break_item.visible = is_started && !break_item.visible; this.set_toggle_state (long_break_item, state == Ft.State.LONG_BREAK); // Windows var screen_overlay_item = this.menu_service.lookup_menu_item ("screen-overlay"); screen_overlay_item.enabled = is_break && !is_finished; screen_overlay_item.visible = is_break && this.application_settings.get_boolean ("screen-overlay"); menu_service.emit_updates (); } /** * Extract icon theme from gresource to cache directory. */ private async void extract_icons (string icons_path, GLib.Cancellable cancellable) throws GLib.Error { if (!this.have_icon_theme) { return; } var icons_theme_path = GLib.Path.build_filename (icons_path, "hicolor"); var directories = new GLib.GenericSet (GLib.str_hash, GLib.str_equal); var sizes = new GLib.GenericSet (GLib.str_hash, GLib.str_equal); foreach (var filename in ICONS) { if (cancellable.is_cancelled ()) { return; } try { var icon_file = GLib.File.new_build_filename ( icons_theme_path, normalize_separators (filename)); var icon_data = GLib.resources_lookup_data ( @"/plugins/sni/icons/$(filename)", GLib.ResourceLookupFlags.NONE); var directory = icon_file.get_parent (); var directory_path = directory.get_path (); if (!directories.contains (directory_path)) { directories.add (directory_path); if (!directory.query_exists ()) { directory.make_directory_with_parents (cancellable); } var filename_parts = filename.split ("/"); if (filename_parts.length == 3) { // size, category, icon name sizes.add (filename_parts[0]); } } yield icon_file.replace_contents_async ( icon_data.get_data (), null, false, GLib.FileCreateFlags.REPLACE_DESTINATION, null, null); } catch (GLib.Error error) { GLib.warning ("Failed to extract %s: %s", filename, error.message); throw error; } } sizes.@foreach ( (size) => { if (size == "scalable") { return; } foreach (var scale in SCALES) { try { var scale_directory = GLib.File.new_for_path ( GLib.Path.build_filename (icons_theme_path, size + scale)); if (!scale_directory.query_exists ()) { scale_directory.make_symbolic_link (size, cancellable); } } catch (GLib.Error error) { GLib.warning ("Failed to make symbolic link: %s", error.message); } } }); } private void export_menu (string menu_object_path, string icons_path) throws Sni.IndicatorError { var root = this.build_menu (); var menu_service = new Sni.DBusMenuService (root, this.action_group, icons_path); try { this.menu_service_id = this.connection.register_object (menu_object_path, menu_service); this.menu_service = menu_service; } catch (GLib.Error error) { throw new Sni.IndicatorError.INDICATOR_MENU (error.message); } this.update_menu_items (); } private void export_icon (string menu_object_path, string icons_path) throws Sni.IndicatorError requires (this.menu_service != null) { var service = new Sni.StatusNotifierItemDBusService (menu_object_path, icons_path); service.received_activation_token.connect ((token) => { if (this.menu_service != null) { this.menu_service.activation_token = token; } }); service.activated.connect ((token) => { var application = Ft.Application.get_default (); var window = application.get_window (); if (window == null || !window.is_active) { this.action_group.activate_action_full ("app.window", new GLib.Variant.string ("timer"), build_platform_data (token)); } else { window.close_to_background (); } }); service.secondary_activated.connect (() => { if (this.have_icon_theme || this.have_tooltips) { // Mimic behaviour of the primary button if (!this.timer.is_finished ()) { this.action_group?.activate_action ("timer.start-pause-resume", null); } else { this.action_group?.activate_action ("session-manager.advance", null); } } }); service.scrolled.connect ((delta) => { if (this.have_tooltips) { this.notification_manager.inhibit (); Ft.Context.set_event_source ("timer.extend"); this.timer.extend (delta > 0 ? Ft.Interval.MINUTE : -Ft.Interval.MINUTE); this.notification_manager.uninhibit (false); } }); this.service = service; var timestamp = this.timer.get_current_time (); this.update_status (); this.update_icon (timestamp); this.update_tooltip (timestamp); try { this.service_id = this.connection.register_object (OBJECT_PATH, service); } catch (GLib.Error error) { this.service = null; this.invalidate_tooltip (); throw new Sni.IndicatorError.INDICATOR (error.message); } } public async void export (GLib.Cancellable cancellable) throws Sni.IndicatorError { var bus_name = BUS_NAME.printf (Config.APPLICATION_ID); var menu_object_path = MENU_OBJECT_PATH.printf ( GLib.Application.get_default ().get_dbus_object_path ()); var icons_path = GLib.Path.build_filename ( GLib.Environment.get_user_cache_dir (), Config.PACKAGE_NAME, "icons"); try { yield this.extract_icons (icons_path, cancellable); this.export_menu (menu_object_path, icons_path); this.export_icon (menu_object_path, icons_path); this.connect_signals (); this.update_icon_timeout (); this.name_owner_id = GLib.Bus.own_name_on_connection ( this.connection, bus_name, GLib.BusNameOwnerFlags.REPLACE, null, null); yield this.watcher_proxy.register_status_notifier_item (bus_name); } catch (GLib.Error error) { throw new Sni.IndicatorError.INDICATOR (error.message); } if (!cancellable.is_cancelled ()) { this.menu_service.emit_layout_updated (); this.service.emit_new_menu (); } } private void update_status () { Sni.IndicatorStatus status; if (this.session_manager.current_state == Ft.State.STOPPED) { status = this.have_passive_status ? Sni.IndicatorStatus.PASSIVE : Sni.IndicatorStatus.ACTIVE; } else if (this.have_attention_status && ( this.timer.is_paused () || this.timer.is_finished ())) { status = Sni.IndicatorStatus.NEEDS_ATTENTION; } else { status = Sni.IndicatorStatus.ACTIVE; } this.service.set_status_internal (status, this.service_id != 0); } private void update_icon (int64 timestamp) { if (!this.have_icon_theme) { return; } string icon_name; var progress = this.timer.calculate_progress (timestamp); var progress_uint = (uint) Math.round (progress * ICON_STEPS) * (100U / ICON_STEPS); switch (this.session_manager.current_state) { case Ft.State.STOPPED: icon_name = "indicator-stopped-symbolic"; break; case Ft.State.POMODORO: icon_name = this.timer.is_paused () ? "indicator-pomodoro-paused-symbolic" : "indicator-pomodoro-%03u-symbolic".printf (progress_uint); break; case Ft.State.BREAK: case Ft.State.SHORT_BREAK: case Ft.State.LONG_BREAK: icon_name = this.timer.is_paused () ? "indicator-break-paused-symbolic" : "indicator-break-%03u-symbolic".printf (progress_uint); break; default: assert_not_reached (); } this.service.set_icon_name_internal (icon_name, this.service_id != 0); } private void update_tooltip (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (!this.have_tooltips) { return; } if (this.tooltip_title == null) { uint cycle_number; uint cycle_count; this.get_cycle_number_count (out cycle_number, out cycle_count); this.tooltip_title = format_tooltip_title ( this.session_manager.current_state, cycle_number, cycle_count, this.timer.is_finished ()); } var tooltip = Sni.Tooltip () { title = this.tooltip_title, }; if (this.have_icon_theme) { tooltip.description = this.timer.duration > 0 && !this.timer.is_finished () ? format_remaining_time (this.timer.calculate_remaining (timestamp)) : ""; } else { tooltip.description = this.timer.is_running () ? format_remaining_time (this.timer.calculate_remaining (timestamp)) : ""; } this.service.set_tooltip_internal (tooltip, this.service_id != 0); } private void update_icon_timeout () { if (this.update_icon_timeout_id != 0) { GLib.Source.remove (this.update_icon_timeout_id); this.update_icon_timeout_id = 0; } this.timer.tick.disconnect (this.update_icon); if (!this.timer.is_running () || !this.have_icon_theme) { return; } var interval = Ft.Timestamp.to_milliseconds_uint (this.timer.duration / ICON_STEPS); if (interval < 5000 && interval > 0) { var offset = Ft.Timestamp.to_milliseconds_uint (this.timer.calculate_elapsed ()) % interval; var deviation = int.min ((int) interval - (int) offset, (int) offset); if (deviation < 100) { this.update_icon_timeout_id = GLib.Timeout.add ( interval, this.on_update_icon_timeout); GLib.Source.set_name_by_id (this.update_icon_timeout_id, "Sni.IndicatorController.update_icon"); } else { this.update_icon_timeout_id = GLib.Timeout.add ( interval - offset, () => { this.on_update_icon_timeout (); this.update_icon_timeout_id = GLib.Timeout.add ( interval, this.on_update_icon_timeout); GLib.Source.set_name_by_id (this.update_icon_timeout_id, "Sni.IndicatorController.update_icon"); return GLib.Source.REMOVE; }); } } else { this.timer.tick.connect (this.update_icon); } } private inline void invalidate_tooltip () { this.tooltip_title = null; } private void update (int64 timestamp) { if (this.update_idle_id != 0) { GLib.Source.remove (this.update_idle_id); this.update_idle_id = 0; } this.update_icon (timestamp); this.update_icon_timeout (); this.update_status (); this.update_menu_items (); this.invalidate_tooltip (); this.update_tooltip (timestamp); } private void queue_update () { if (this.update_idle_id != 0) { return; } this.update_idle_id = GLib.Idle.add ( () => { var timestamp = this.timer.get_current_time (GLib.MainContext.current_source ().get_time ()); this.update_idle_id = 0; this.update (timestamp); return GLib.Source.REMOVE; }); } private bool on_update_icon_timeout () { var timestamp = this.timer.get_current_time (GLib.MainContext.current_source ().get_time ()); this.update_icon (timestamp); return GLib.Source.CONTINUE; } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update (this.timer.get_last_state_changed_time ()); } private void on_session_manager_notify_current_session () { this.queue_update (); } private void on_session_manager_notify_current_state () { this.queue_update (); } private void on_session_manager_notify_has_uniform_breaks () { this.queue_update (); } private void on_application_settings_changed (GLib.Settings settings, string key) { switch (key) { case "screen-overlay": this.update_menu_items (); break; default: break; } } private void connect_signals () { this.timer.state_changed.connect (this.on_timer_state_changed); if (this.have_tooltips) { this.timer.tick.connect (this.update_tooltip); } this.session_manager.notify["current-session"].connect (this.on_session_manager_notify_current_session); this.session_manager.notify["current-state"].connect (this.on_session_manager_notify_current_state); this.session_manager.notify["has-uniform-breaks"].connect (this.on_session_manager_notify_has_uniform_breaks); this.application_settings.changed.connect (this.on_application_settings_changed); } private void disconnect_signals () { if (this.timer != null) { this.timer.state_changed.disconnect (this.on_timer_state_changed); this.timer.tick.disconnect (this.update_tooltip); this.timer.tick.disconnect (this.update_icon); } if (this.session_manager != null) { this.session_manager.notify["current-session"].disconnect (this.on_session_manager_notify_current_session); this.session_manager.notify["current-state"].disconnect (this.on_session_manager_notify_current_state); this.session_manager.notify["has-uniform-breaks"].disconnect (this.on_session_manager_notify_has_uniform_breaks); } if (this.application_settings != null) { this.application_settings.changed.disconnect (this.on_application_settings_changed); } } /** * Unexport services and cleanup. */ public void destroy () { this.disconnect_signals (); if (this.update_icon_timeout_id != 0) { GLib.Source.remove (this.update_icon_timeout_id); this.update_icon_timeout_id = 0; } if (this.update_idle_id != 0) { GLib.Source.remove (this.update_idle_id); this.update_idle_id = 0; } if (this.service_id != 0) { this.connection.unregister_object (this.service_id); this.service_id = 0; } if (this.menu_service_id != 0) { this.connection.unregister_object (this.menu_service_id); this.menu_service_id = 0; } if (this.name_owner_id != 0) { GLib.Bus.unown_name (this.name_owner_id); this.name_owner_id = 0; } } } public class IndicatorProvider : Ft.Provider, Ft.IndicatorProvider { public bool visible { get { return this.indicator_controller != null; } } private GLib.Settings? settings = null; private GLib.DBusConnection? connection = null; private uint watcher_id = 0; private Sni.StatusNotifierWatcher? watcher_proxy = null; private Sni.IndicatorController? indicator_controller = null; private GLib.Cancellable? cancellable = null; private void update_available () { this.available = this.watcher_proxy != null && this.watcher_proxy.is_status_notifier_host_registered; } private async void create_indicator_controller () { if (this.indicator_controller != null) { return; } var application = Ft.Application.get_default (); application.hold (); this.indicator_controller = new Sni.IndicatorController (this.connection, this.watcher_proxy); try { yield this.indicator_controller.export (this.cancellable); this.notify_property ("visible"); } catch (GLib.Error error) { GLib.warning ("Failed to export SNI: %s", error.message); this.destroy_indicator_controller (); } application.release (); } private void destroy_indicator_controller () { if (this.indicator_controller != null) { this.indicator_controller.destroy (); this.indicator_controller = null; this.notify_property ("visible"); } } private void on_properties_changed (GLib.Variant changed_properties, string[] invalidated_properties) { this.update_available (); } private void on_settings_changed (GLib.Settings settings, string key) { switch (key) { case "indicator": if (settings.get_boolean (key)) { this.create_indicator_controller.begin (); } else { var application = Ft.Application.get_default (); application.hold (); this.destroy_indicator_controller (); // Ensure there is a main window when disabling the indicator. // It undoes the close-to-tray behaviour. var main_window = application.get_window (); if (main_window == null) { application.show_window (); } application.release (); } break; default: break; } } private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { if (this.watcher_proxy != null) { return; } try { this.watcher_proxy = GLib.Bus.get_proxy_sync ( GLib.BusType.SESSION, "org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher", GLib.DBusProxyFlags.DO_NOT_AUTO_START, this.cancellable); var watcher_proxy = (GLib.DBusProxy) this.watcher_proxy; watcher_proxy.g_properties_changed.connect (this.on_properties_changed); this.update_available (); } catch (GLib.Error error) { GLib.warning ("Error while initializing StatusNotifierWatcher proxy: %s", error.message); } } private void on_name_vanished (GLib.DBusConnection? connection, string name) { if (this.watcher_proxy != null) { var watcher_proxy = (GLib.DBusProxy) this.watcher_proxy; watcher_proxy.g_properties_changed.disconnect (this.on_properties_changed); this.watcher_proxy = null; } this.available = false; this.enabled = false; } /** * Mark provider as `available` when `org.kde.StatusNotifierWatcher` exists * and has a host registered. */ public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.connection = GLib.Application.get_default ().get_dbus_connection (); if (this.connection?.get_unique_name () == null) { throw new Sni.IndicatorError.CONNECTION ("No connection"); } if (this.watcher_id == 0) { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.kde.StatusNotifierWatcher", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.cancellable = cancellable ?? new GLib.Cancellable (); this.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer.plugins.sni"); this.settings.changed.connect (this.on_settings_changed); if (this.settings.get_boolean ("indicator")) { yield this.create_indicator_controller (); } } public override async void disable () throws GLib.Error { if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } if (this.settings != null) { this.settings.changed.disconnect (this.on_settings_changed); this.settings = null; } this.destroy_indicator_controller (); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } this.connection = null; } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/interfaces.vala000066400000000000000000000155371520625676500247230ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { [DBus (name = "org.kde.StatusNotifierWatcher")] public interface StatusNotifierWatcher : GLib.Object { public abstract bool is_status_notifier_host_registered { get; } public abstract int32 protocol_version { get; } public abstract async void register_status_notifier_item (string service) throws GLib.DBusError, GLib.IOError; } public enum IndicatorStatus { PASSIVE, ACTIVE, NEEDS_ATTENTION; public string to_string () { switch (this) { case PASSIVE: return "Passive"; case ACTIVE: return "Active"; case NEEDS_ATTENTION: return "NeedsAttention"; default: assert_not_reached (); } } public static Sni.IndicatorStatus from_string (string? status) { switch (status) { case "Passive": return PASSIVE; case "NeedsAttention": return NEEDS_ATTENTION; default: return ACTIVE; } } } public struct Pixmap { public int32 width; public int32 height; public uint8[] data; public GLib.Variant to_variant () { return new GLib.Variant.tuple ({ new GLib.Variant.int32 (this.width), new GLib.Variant.int32 (this.height), new GLib.Variant.from_bytes (GLib.VariantType.BYTE, new GLib.Bytes (this.data), true), }); } } public struct Tooltip { public string icon_name; public Pixmap[] icon_pixmaps; public string title; public string description; // supports markup public Tooltip () { this.icon_name = ""; this.icon_pixmaps = {}; this.title = ""; this.description = ""; } public GLib.Variant to_variant () { return new GLib.Variant.tuple ({ new GLib.Variant.string (this.icon_name), serialize_pixmaps (this.icon_pixmaps), new GLib.Variant.string (this.title), new GLib.Variant.string (this.description), }); } public bool equals (Sni.Tooltip other) { return this.description == other.description && this.title == other.title && this.icon_name == other.icon_name; } } public enum MenuToggleType { NONE, CHECKMARK, RADIO; public string to_string () { switch (this) { case NONE: return ""; case CHECKMARK: return "checkmark"; case RADIO: return "radio"; default: assert_not_reached (); } } } public enum MenuItemType { STANDARD, SEPARATOR; public string to_string () { switch (this) { case STANDARD: return "standard"; case SEPARATOR: return "separator"; default: assert_not_reached (); } } } /** * Menu item properties exported in the D-Bus API */ public enum MenuItemProperty { INVALID, TYPE, LABEL, ENABLED, VISIBLE, ICON_NAME, TOGGLE_TYPE, TOGGLE_STATE, CHILDREN_DISPLAY, ICON_DATA, SHORTCUT; public static MenuItemProperty from_string (string str) { switch (str) { case "type": return TYPE; case "label": return LABEL; case "enabled": return ENABLED; case "visible": return VISIBLE; case "icon-name": return ICON_NAME; case "toggle-type": return TOGGLE_TYPE; case "toggle-state": return TOGGLE_STATE; case "children-display": return CHILDREN_DISPLAY; case "icon-data": return ICON_DATA; case "shortcut": return SHORTCUT; default: return INVALID; } } public static Sni.MenuItemProperty[] from_strv (string[] strv) { if (strv.length == 0) { return all (); } else { var properties = new Sni.MenuItemProperty[strv.length]; for (var index = 0; index < strv.length; index++) { properties[index] = Sni.MenuItemProperty.from_string (strv[index]); } return properties; } } public static Sni.MenuItemProperty[] all () { return { Sni.MenuItemProperty.TYPE, Sni.MenuItemProperty.LABEL, Sni.MenuItemProperty.ENABLED, Sni.MenuItemProperty.VISIBLE, Sni.MenuItemProperty.ICON_NAME, Sni.MenuItemProperty.TOGGLE_TYPE, Sni.MenuItemProperty.TOGGLE_STATE, Sni.MenuItemProperty.CHILDREN_DISPLAY, // Sni.MenuItemProperty.ICON_DATA, // Sni.MenuItemProperty.SHORTCUT, }; } public string to_string () { switch (this) { case TYPE: return "type"; case LABEL: return "label"; case ENABLED: return "enabled"; case VISIBLE: return "visible"; case ICON_NAME: return "icon-name"; case TOGGLE_TYPE: return "toggle-type"; case TOGGLE_STATE: return "toggle-state"; case CHILDREN_DISPLAY: return "children-display"; case ICON_DATA: return "icon-data"; case SHORTCUT: return "shortcut"; case INVALID: return ""; default: assert_not_reached (); } } } } io.github.focustimerhq.FocusTimer.plugins.sni.gschema.xml000066400000000000000000000004361520625676500347230ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/sni true Whether to enable indicator focustimerhq-FocusTimer-8581be2/src/plugins/sni/menu-item.vala000066400000000000000000000233041520625676500244670ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { public sealed class MenuItem : GLib.Object { public uint id { get; construct; } public string name { get; construct; } public string action_name { get; set; default = ""; } public GLib.Variant? action_target { get; set; } [CCode (notify = false)] public Sni.MenuItemType item_type { get { return this._item_type; } construct { this._item_type = value; } } [CCode (notify = false)] public string icon_name { get { return this._icon_name; } set { if (this._icon_name != value) { this._icon_name = value; this.changed (Sni.MenuItemProperty.ICON_NAME); } } } [CCode (notify = false)] public string label { get { return this._label; } set { if (this._label != value) { this._label = value; this.changed (Sni.MenuItemProperty.LABEL); } } } [CCode (notify = false)] public Sni.MenuToggleType toggle_type { get { return this._toggle_type; } set { if (this._toggle_type != value) { this._toggle_type = value; this.changed (Sni.MenuItemProperty.TOGGLE_TYPE); this.changed (Sni.MenuItemProperty.TOGGLE_STATE); } } } [CCode (notify = false)] public bool toggle_state { get { return this._toggle_state; } set { if (this._toggle_state != value) { this._toggle_state = value; this.changed (Sni.MenuItemProperty.TOGGLE_STATE); } } } [CCode (notify = false)] public bool enabled { get { return this._enabled; } set { if (this._enabled != value) { this._enabled = value; this.changed (Sni.MenuItemProperty.ENABLED); } } } [CCode (notify = false)] public bool visible { get { return this._visible; } set { if (this._visible != value) { this._visible = value; this.changed (Sni.MenuItemProperty.VISIBLE); } } } private static uint next_id = 1U; private Sni.MenuItemType _item_type = Sni.MenuItemType.STANDARD; private string _icon_name = ""; private string _label = ""; private Sni.MenuToggleType _toggle_type = Sni.MenuToggleType.NONE; private bool _toggle_state = false; private bool _enabled = true; private bool _visible = true; private weak Sni.MenuItem? parent = null; private GLib.List children; internal bool dirty_layout = false; internal uint dirty_properties = 0U; internal uint modified_properties = 0U; public MenuItem (string name, string label, string icon_name, string action_name, GLib.Variant? action_target = null) { var id = next_id; next_id++; GLib.Object ( id: id, name: name, item_type: Sni.MenuItemType.STANDARD, label: label, icon_name: icon_name, action_name: action_name, action_target: action_target ); } public MenuItem.root () { GLib.Object ( id: 0U, name: "", item_type: Sni.MenuItemType.STANDARD ); } public MenuItem.separator (string name = "") { var id = next_id; next_id++; GLib.Object ( id: id, name: name, item_type: Sni.MenuItemType.SEPARATOR ); } public void append (Sni.MenuItem child) requires (child.parent == null) { child.parent = this; this.children.append (child); this.child_added (child); } public void @foreach (GLib.Func func) { this.children.@foreach (func); } public void traverse (GLib.Func func) { func (this); this.children.@foreach ( (child) => { child.traverse (func); }); } public bool has_children () { return this.children != null; } public bool needs_update () { // XXX: check children if they need update? return this.dirty_layout || this.dirty_properties != 0U; } internal bool has_default_property (Sni.MenuItemProperty property) { switch (property) { case Sni.MenuItemProperty.TYPE: return this._item_type == Sni.MenuItemType.STANDARD; case Sni.MenuItemProperty.LABEL: return this._label == ""; case Sni.MenuItemProperty.ENABLED: return this._enabled; case Sni.MenuItemProperty.VISIBLE: return this._visible; case Sni.MenuItemProperty.ICON_NAME: return this._icon_name == ""; case Sni.MenuItemProperty.TOGGLE_TYPE: case Sni.MenuItemProperty.TOGGLE_STATE: return this._toggle_type == Sni.MenuToggleType.NONE; case Sni.MenuItemProperty.CHILDREN_DISPLAY: return !this.has_children (); case Sni.MenuItemProperty.ICON_DATA: return true; case Sni.MenuItemProperty.SHORTCUT: return true; case Sni.MenuItemProperty.INVALID: return true; default: assert_not_reached (); } } internal bool has_modified_property (Sni.MenuItemProperty property) { return (this.modified_properties & (1 << (uint) property)) != 0; } public GLib.Variant? serialize_property (Sni.MenuItemProperty property) { switch (property) { case Sni.MenuItemProperty.TYPE: return new GLib.Variant.string (this._item_type.to_string ()); case Sni.MenuItemProperty.LABEL: return new GLib.Variant.string (this._label); case Sni.MenuItemProperty.ENABLED: return new GLib.Variant.boolean (this._enabled); case Sni.MenuItemProperty.VISIBLE: return new GLib.Variant.boolean (this._visible); case Sni.MenuItemProperty.ICON_NAME: return new GLib.Variant.string (this._icon_name); case Sni.MenuItemProperty.TOGGLE_TYPE: return new GLib.Variant.string (this._toggle_type.to_string ()); case Sni.MenuItemProperty.TOGGLE_STATE: return new GLib.Variant.int32 (this._toggle_type != Sni.MenuToggleType.NONE ? (int32) this._toggle_state : -1); case Sni.MenuItemProperty.CHILDREN_DISPLAY: return new GLib.Variant.string (this.has_children () ? "submenu" : ""); case Sni.MenuItemProperty.ICON_DATA: // unused return null; case Sni.MenuItemProperty.SHORTCUT: // unused return null; case Sni.MenuItemProperty.INVALID: return null; default: assert_not_reached (); } } public GLib.Variant? serialize_properties (Sni.MenuItemProperty[] properties) { var builder = new GLib.VariantBuilder (GLib.VariantType.VARDICT); foreach (var property in properties) { var value = this.serialize_property (property); if (value != null) { builder.add ("{sv}", property.to_string (), value); } } return builder.end (); } internal void mark_dirty_layout () { this.dirty_layout = true; } internal void mark_dirty_property (Sni.MenuItemProperty property) { uint property_mask = 1 << (uint) property; this.dirty_properties |= property_mask; this.modified_properties |= property_mask; } public signal void child_added (Sni.MenuItem child); public signal void child_removed (Sni.MenuItem child); public signal void changed (Sni.MenuItemProperty property); public override void dispose () { this.parent = null; this.children = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/meson.build000066400000000000000000000021401520625676500240570ustar00rootroot00000000000000if get_option('plugin_sni').enabled() sni_plugin_resources = gnome.compile_resources( 'sni-plugin-resources', 'sni.gresource.xml', c_name: 'sni_plugin', ) libft_plugin_sni = static_library( 'ft_plugin_sni', files( 'capabilities.vala', 'indicator-action-group.vala', 'indicator-provider.vala', 'interfaces.vala', 'dbus-services.vala', 'menu-item.vala', 'preferences-window-extension.vala', 'sni.vala', ) + sni_plugin_resources, dependencies: [libft_ui_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_sni] # GSchema install_data( 'io.github.focustimerhq.FocusTimer.plugins.sni.gschema.xml', install_dir: gschema_dir, ) compile_schemas = find_program('glib-compile-schemas', required: false) if compile_schemas.found() test('Validate sni schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()] ) endif compiled_schemas = gnome.compile_schemas( depend_files: 'io.github.focustimerhq.FocusTimer.plugins.sni.gschema.xml' ) endiffocustimerhq-FocusTimer-8581be2/src/plugins/sni/preferences-window-extension.vala000066400000000000000000000107401520625676500304070ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { /** * Offer to disable the indicator as it may be buggy or less useful with some hosts. * Other indicator implementations can't be disabled, so the toggle is specific to * SNI plugin. */ public class PreferencesWindowExtension : Ft.PreferencesWindowExtension { private GLib.Settings? settings = null; private Ft.Indicator? indicator = null; private Ft.PreferencesPanel? last_panel = null; private unowned Adw.PreferencesGroup? indicator_group = null; construct { this.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer.plugins.sni"); this.indicator = new Ft.Indicator (); this.indicator.notify["provider"].connect (this.on_notify_provider); this.notify["window"].connect (this.on_notify_window); } private void setup_appearance_panel (Ft.PreferencesPanel panel) { var page = panel.get_preferences_page (); if (this.settings == null) { this.taredown_appearance_panel (panel); return; } if (this.indicator_group == null) { var indicator_group = new Adw.PreferencesGroup (); indicator_group.title = _("System Tray Icon"); page.add (indicator_group); var indicator_row = new Adw.SwitchRow (); indicator_row.title = _("Show Tray Icon"); indicator_row.subtitle = _("Closing the window keeps the app running in the background."); indicator_group.add (indicator_row); this.settings.bind ("indicator", indicator_row, "active", GLib.SettingsBindFlags.DEFAULT); this.indicator_group = indicator_group; } } private void taredown_appearance_panel (Ft.PreferencesPanel panel) { this.indicator_group?.unparent (); this.indicator_group = null; } private void setup () { var panel = this.window?.visible_panel; if (panel != this.last_panel) { switch (this.last_panel?.tag) { case "appearance": this.taredown_appearance_panel (this.last_panel); break; } this.last_panel = panel; } switch (panel?.tag) { case "appearance": this.setup_appearance_panel (panel); break; default: this.taredown (); break; } } private void taredown () { if (this.last_panel == null) { return; } this.taredown_appearance_panel (this.last_panel); this.last_panel = null; } private void update () { if (this.indicator?.provider is Sni.IndicatorProvider) { this.setup (); } else { this.taredown (); } } private void on_notify_provider (GLib.Object object, GLib.ParamSpec pspec) { this.update (); } private void on_notify_visible_panel (GLib.Object object, GLib.ParamSpec pspec) { this.update (); } private void on_notify_window (GLib.Object object, GLib.ParamSpec pspec) { if (this.window != null) { this.window.notify["visible-panel"].connect (this.on_notify_visible_panel); } } public override void dispose () { this.taredown (); if (this.window != null) { this.window.notify["visible-panel"].disconnect (this.on_notify_visible_panel); } if (this.indicator != null) { this.indicator.notify["provider"].disconnect (this.on_notify_provider); this.indicator = null; } this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/sni/sni.gresource.xml000066400000000000000000000270201520625676500252310ustar00rootroot00000000000000 icons/16x16/status/indicator-break-000-symbolic.svg icons/16x16/status/indicator-break-005-symbolic.svg icons/16x16/status/indicator-break-010-symbolic.svg icons/16x16/status/indicator-break-015-symbolic.svg icons/16x16/status/indicator-break-020-symbolic.svg icons/16x16/status/indicator-break-025-symbolic.svg icons/16x16/status/indicator-break-030-symbolic.svg icons/16x16/status/indicator-break-035-symbolic.svg icons/16x16/status/indicator-break-040-symbolic.svg icons/16x16/status/indicator-break-045-symbolic.svg icons/16x16/status/indicator-break-050-symbolic.svg icons/16x16/status/indicator-break-055-symbolic.svg icons/16x16/status/indicator-break-060-symbolic.svg icons/16x16/status/indicator-break-065-symbolic.svg icons/16x16/status/indicator-break-070-symbolic.svg icons/16x16/status/indicator-break-075-symbolic.svg icons/16x16/status/indicator-break-080-symbolic.svg icons/16x16/status/indicator-break-085-symbolic.svg icons/16x16/status/indicator-break-090-symbolic.svg icons/16x16/status/indicator-break-095-symbolic.svg icons/16x16/status/indicator-break-100-symbolic.svg icons/16x16/status/indicator-break-paused-symbolic.svg icons/16x16/status/indicator-pomodoro-000-symbolic.svg icons/16x16/status/indicator-pomodoro-005-symbolic.svg icons/16x16/status/indicator-pomodoro-010-symbolic.svg icons/16x16/status/indicator-pomodoro-015-symbolic.svg icons/16x16/status/indicator-pomodoro-020-symbolic.svg icons/16x16/status/indicator-pomodoro-025-symbolic.svg icons/16x16/status/indicator-pomodoro-030-symbolic.svg icons/16x16/status/indicator-pomodoro-035-symbolic.svg icons/16x16/status/indicator-pomodoro-040-symbolic.svg icons/16x16/status/indicator-pomodoro-045-symbolic.svg icons/16x16/status/indicator-pomodoro-050-symbolic.svg icons/16x16/status/indicator-pomodoro-055-symbolic.svg icons/16x16/status/indicator-pomodoro-060-symbolic.svg icons/16x16/status/indicator-pomodoro-065-symbolic.svg icons/16x16/status/indicator-pomodoro-070-symbolic.svg icons/16x16/status/indicator-pomodoro-075-symbolic.svg icons/16x16/status/indicator-pomodoro-080-symbolic.svg icons/16x16/status/indicator-pomodoro-085-symbolic.svg icons/16x16/status/indicator-pomodoro-090-symbolic.svg icons/16x16/status/indicator-pomodoro-095-symbolic.svg icons/16x16/status/indicator-pomodoro-100-symbolic.svg icons/16x16/status/indicator-pomodoro-paused-symbolic.svg icons/16x16/status/indicator-stopped-symbolic.svg icons/22x22/status/indicator-break-000-symbolic.svg icons/22x22/status/indicator-break-005-symbolic.svg icons/22x22/status/indicator-break-010-symbolic.svg icons/22x22/status/indicator-break-015-symbolic.svg icons/22x22/status/indicator-break-020-symbolic.svg icons/22x22/status/indicator-break-025-symbolic.svg icons/22x22/status/indicator-break-030-symbolic.svg icons/22x22/status/indicator-break-035-symbolic.svg icons/22x22/status/indicator-break-040-symbolic.svg icons/22x22/status/indicator-break-045-symbolic.svg icons/22x22/status/indicator-break-050-symbolic.svg icons/22x22/status/indicator-break-055-symbolic.svg icons/22x22/status/indicator-break-060-symbolic.svg icons/22x22/status/indicator-break-065-symbolic.svg icons/22x22/status/indicator-break-070-symbolic.svg icons/22x22/status/indicator-break-075-symbolic.svg icons/22x22/status/indicator-break-080-symbolic.svg icons/22x22/status/indicator-break-085-symbolic.svg icons/22x22/status/indicator-break-090-symbolic.svg icons/22x22/status/indicator-break-095-symbolic.svg icons/22x22/status/indicator-break-100-symbolic.svg icons/22x22/status/indicator-break-paused-symbolic.svg icons/22x22/status/indicator-pomodoro-000-symbolic.svg icons/22x22/status/indicator-pomodoro-005-symbolic.svg icons/22x22/status/indicator-pomodoro-010-symbolic.svg icons/22x22/status/indicator-pomodoro-015-symbolic.svg icons/22x22/status/indicator-pomodoro-020-symbolic.svg icons/22x22/status/indicator-pomodoro-025-symbolic.svg icons/22x22/status/indicator-pomodoro-030-symbolic.svg icons/22x22/status/indicator-pomodoro-035-symbolic.svg icons/22x22/status/indicator-pomodoro-040-symbolic.svg icons/22x22/status/indicator-pomodoro-045-symbolic.svg icons/22x22/status/indicator-pomodoro-050-symbolic.svg icons/22x22/status/indicator-pomodoro-055-symbolic.svg icons/22x22/status/indicator-pomodoro-060-symbolic.svg icons/22x22/status/indicator-pomodoro-065-symbolic.svg icons/22x22/status/indicator-pomodoro-070-symbolic.svg icons/22x22/status/indicator-pomodoro-075-symbolic.svg icons/22x22/status/indicator-pomodoro-080-symbolic.svg icons/22x22/status/indicator-pomodoro-085-symbolic.svg icons/22x22/status/indicator-pomodoro-090-symbolic.svg icons/22x22/status/indicator-pomodoro-095-symbolic.svg icons/22x22/status/indicator-pomodoro-100-symbolic.svg icons/22x22/status/indicator-pomodoro-paused-symbolic.svg icons/22x22/status/indicator-stopped-symbolic.svg icons/scalable/actions/ornament-check-symbolic.svg icons/scalable/actions/ornament-dot-checked-symbolic.svg icons/scalable/actions/ornament-dot-unchecked-symbolic.svg icons/scalable/actions/timer-pause-symbolic.svg icons/scalable/actions/timer-reset-symbolic.svg icons/scalable/actions/timer-skip-symbolic.svg icons/scalable/actions/timer-skip-symbolic-rtl.svg icons/scalable/actions/timer-start-symbolic.svg icons/scalable/actions/timer-stop-symbolic.svg icons/index.theme sni.plugin focustimerhq-FocusTimer-8581be2/src/plugins/sni/sni.plugin000066400000000000000000000001321520625676500237250ustar00rootroot00000000000000[Plugin] Name=SNI Module=sni Builtin=true Embedded=sni_peas_register_types X-Priority=low focustimerhq-FocusTimer-8581be2/src/plugins/sni/sni.vala000066400000000000000000000011341520625676500233550ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Sni { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; object_module.register_extension_type (typeof (Ft.PreferencesWindowExtension), typeof (Sni.PreferencesWindowExtension)); object_module.register_extension_type (typeof (Ft.IndicatorProvider), typeof (Sni.IndicatorProvider)); } } focustimerhq-FocusTimer-8581be2/src/plugins/wayland/000077500000000000000000000000001520625676500225665ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/wayland/ft-wayland.deps000066400000000000000000000000171520625676500255070ustar00rootroot00000000000000wayland-client focustimerhq-FocusTimer-8581be2/src/plugins/wayland/ft-wayland.h000066400000000000000000000037331520625676500250130ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include #include G_BEGIN_DECLS typedef struct _FtWaylandIdleMonitor FtWaylandIdleMonitor; typedef void (*FtWaylandIdleMonitorCallback) (uint32_t id, gpointer user_data); FtWaylandIdleMonitor *ft_wayland_idle_monitor_new (struct wl_display *display, struct wl_seat *seat); FtWaylandIdleMonitor *ft_wayland_idle_monitor_ref (FtWaylandIdleMonitor *monitor); void ft_wayland_idle_monitor_unref (FtWaylandIdleMonitor *monitor); void ft_wayland_idle_monitor_free (FtWaylandIdleMonitor *monitor); gboolean ft_wayland_idle_monitor_is_ready (FtWaylandIdleMonitor *monitor); gboolean ft_wayland_idle_monitor_supports_input_idle (FtWaylandIdleMonitor *monitor); uint32_t ft_wayland_idle_monitor_add_notification (FtWaylandIdleMonitor *monitor, uint32_t timeout_ms, FtWaylandIdleMonitorCallback on_idled, FtWaylandIdleMonitorCallback on_resumed, gpointer user_data); uint32_t ft_wayland_idle_monitor_add_input_notification (FtWaylandIdleMonitor *monitor, uint32_t timeout_ms, FtWaylandIdleMonitorCallback on_idled, FtWaylandIdleMonitorCallback on_resumed, gpointer user_data); void ft_wayland_idle_monitor_remove_notification (FtWaylandIdleMonitor *monitor, uint32_t id); G_END_DECLS focustimerhq-FocusTimer-8581be2/src/plugins/wayland/ft-wayland.vapi000066400000000000000000000022011520625676500255100ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ [CCode (cheader_filename = "ft-wayland.h")] namespace FtWayland { [CCode (has_target = false)] public delegate void IdleMonitorCallback (uint32 id, void* user_data); public class IdleMonitor { public IdleMonitor (Wl.Display display, Wl.Seat seat); public bool is_ready (); public bool supports_input_idle (); public uint32 add_notification (uint32 timeout_ms, IdleMonitorCallback? on_idled, IdleMonitorCallback? on_resumed, void* user_data); public uint32 add_input_notification (uint32 timeout_ms, IdleMonitorCallback? on_idled, IdleMonitorCallback? on_resumed, void* user_data); public void remove_notification (uint32 id); } } focustimerhq-FocusTimer-8581be2/src/plugins/wayland/idle-monitor-provider.vala000066400000000000000000000277631520625676500277040ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Wayland { public class IdleMonitorProvider : Ft.Provider, Ft.IdleMonitorProvider { private const int64 TIMEOUT_TOLERANCE = 100 * Ft.Interval.MILLISECOND; [Compact] class Watch { public uint32 id = 0; public int64 absolute_timeout = 0; public int64 relative_timeout = 0; public int64 reference_time = Ft.Timestamp.UNDEFINED; public bool ignore_inhibitors = false; public bool has_active_watch = false; public bool invalid = false; } public bool can_ignore_inhibitors { get { return this.supports_input_idle; } } private bool supports_input_idle = false; private FtWayland.IdleMonitor? idle_monitor = null; private GLib.HashTable watches = null; private uint32 active_watch_id = 0; private uint active_watch_use_count = 0; construct { this.watches = new GLib.HashTable (int64_hash, int64_equal); } private inline uint64 to_milliseconds (int64 interval) { return (uint64) int64.max (interval, 0) / Ft.Interval.MILLISECOND; } private void remove_active_watch_internal () throws GLib.Error { if (this.active_watch_id != 0 && this.idle_monitor != null) { var watch_id = this.active_watch_id; this.active_watch_id = 0; this.active_watch_use_count = 0; this.idle_monitor.remove_notification (watch_id); } } private void on_became_active () { if (!this.enabled) { return; } try { this.remove_active_watch_internal (); } catch (GLib.Error error) { GLib.warning ("Error while removing active-watch: %s", error.message); } this.became_active (); } private void on_became_idle (Watch watch) { if (!this.enabled) { return; } var monotonic_time = GLib.get_monotonic_time (); var min_elapsed = int64.max (watch.relative_timeout - TIMEOUT_TOLERANCE, watch.relative_timeout / 2); if (monotonic_time - watch.reference_time >= min_elapsed) { this.became_idle (watch.id); } } [CCode (has_target = false)] private static void on_notification_idled (uint32 id, void* user_data) { var provider = user_data as IdleMonitorProvider; if (provider == null || provider.idle_monitor == null) { return; } provider.handle_notification_idled (id); } [CCode (has_target = false)] private static void on_notification_resumed (uint32 id, void* user_data) { var provider = user_data as IdleMonitorProvider; if (provider == null || provider.idle_monitor == null) { return; } provider.handle_notification_resumed (id); } private void handle_notification_idled (uint32 id) { if (id == 0 || id == this.active_watch_id) { return; } unowned var watch = this.watches.lookup (id); if (watch != null && !watch.invalid) { this.on_became_idle (watch); } } private void handle_notification_resumed (uint32 id) { if (id != 0 && id == this.active_watch_id) { this.on_became_active (); } } private void update_available () { var display = Gdk.Display.get_default () as Gdk.Wayland.Display; if (display == null) { this.available = false; } var seat = display.get_default_seat () as Gdk.Wayland.Seat; if (seat == null) { this.available = false; return; } if (this.idle_monitor == null) { this.idle_monitor = new FtWayland.IdleMonitor (display.get_wl_display (), seat.get_wl_seat ()); } if (this.idle_monitor.is_ready ()) { this.supports_input_idle = this.idle_monitor.supports_input_idle (); this.available = true; } else { this.available = false; } } public uint32 add_idle_watch (int64 timeout, bool ignore_inhibitors, int64 monotonic_time) throws GLib.Error requires (this.idle_monitor != null) { int64 relative_timeout = timeout; int64 absolute_timeout = timeout; uint32 watch_id; if (Ft.Timestamp.is_undefined (monotonic_time)) { monotonic_time = GLib.get_monotonic_time () - relative_timeout; } else { absolute_timeout = Ft.IdleMonitorProvider.calculate_absolute_timeout ( relative_timeout, 0, monotonic_time); if ((absolute_timeout - relative_timeout).abs () < TIMEOUT_TOLERANCE) { absolute_timeout = relative_timeout; } } if (ignore_inhibitors && !this.can_ignore_inhibitors) { ignore_inhibitors = false; } if (ignore_inhibitors) { watch_id = this.idle_monitor.add_input_notification ( (uint32) this.to_milliseconds (timeout), on_notification_idled, null, this); } else { watch_id = this.idle_monitor.add_notification ( (uint32) this.to_milliseconds (timeout), on_notification_idled, null, this); } var watch = new Watch (); watch.id = watch_id; watch.relative_timeout = relative_timeout; watch.absolute_timeout = absolute_timeout; watch.reference_time = monotonic_time; watch.ignore_inhibitors = ignore_inhibitors; unowned var _watch = watch; this.watches.insert (watch_id, (owned) watch); if (!_watch.has_active_watch && _watch.absolute_timeout != _watch.relative_timeout) { try { this.add_active_watch (); _watch.has_active_watch = true; } catch (GLib.Error error) { GLib.debug ("Unable to add active watch: %s", error.message); this.remove_idle_watch (watch_id); throw error; } } return watch_id; } public void remove_idle_watch (uint32 id) requires (this.idle_monitor != null) { unowned var watch = this.watches.lookup (id); if (watch == null) { return; } watch.invalid = true; if (watch.has_active_watch) { try { this.remove_active_watch (); watch.has_active_watch = false; } catch (GLib.Error error) { GLib.warning ("Unable to remove active watch: %s", error.message); } } this.idle_monitor.remove_notification (watch.id); if (!watch.has_active_watch) { this.watches.remove (id); } } public uint32 reset_idle_watch (uint32 id, int64 monotonic_time) throws GLib.Error requires (this.idle_monitor != null) { unowned var watch = this.watches.lookup (id); if (watch == null || watch.absolute_timeout == watch.relative_timeout) { return id; } var new_id = this.add_idle_watch (watch.relative_timeout, watch.ignore_inhibitors, monotonic_time); this.idle_monitor.remove_notification (watch.id); this.watches.remove (id); return new_id; } public void add_active_watch () throws GLib.Error requires (this.idle_monitor != null) { if (this.active_watch_id == 0) { var ignore_inhibitors = this.can_ignore_inhibitors; if (ignore_inhibitors) { this.active_watch_id = this.idle_monitor.add_input_notification ( 0, null, on_notification_resumed, this); } else { this.active_watch_id = this.idle_monitor.add_notification ( 0, null, on_notification_resumed, this); } var active_watch = new Watch (); active_watch.id = this.active_watch_id; active_watch.ignore_inhibitors = ignore_inhibitors; this.watches.insert (this.active_watch_id, (owned) active_watch); } this.active_watch_use_count++; } public void remove_active_watch () throws GLib.Error requires (this.active_watch_use_count > 0) { if (this.active_watch_use_count > 1) { this.active_watch_use_count--; } else if (this.active_watch_use_count == 1) { var watch_id = this.active_watch_id; this.remove_active_watch_internal (); if (watch_id != 0) { this.watches.remove (watch_id); } } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { var display = Gdk.Display.get_default (); if (display == null) { GLib.debug ("Wayland idle monitor: no default Gdk display"); return; } this.update_available (); } public override async void uninitialize () throws GLib.Error { this.available = false; this.idle_monitor = null; } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { assert (this.idle_monitor != null); } public override async void disable () throws GLib.Error { if (this.idle_monitor == null) { return; } uint32[] ids = {}; this.watches.@foreach ( (id, watch) => { ids += watch.id; }); for (var index = 0; index < ids.length; index++) { this.remove_idle_watch (ids[index]); } this.remove_active_watch_internal (); } public override void dispose () { this.watches = null; this.idle_monitor = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/plugins/wayland/idle-monitor.c000066400000000000000000000220061520625676500253340ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include "ft-wayland.h" #include "ext-idle-protocol-client.h" #define FT_WAYLAND_IDLE_MONITOR_BIND_VERSION 2 typedef struct { uint32_t id; uint32_t timeout_ms; struct ext_idle_notification_v1 *notification; FtWaylandIdleMonitorCallback on_idled; FtWaylandIdleMonitorCallback on_resumed; gpointer user_data; } FtWaylandIdleMonitorWatch; struct _FtWaylandIdleMonitor { struct wl_display *display; struct wl_seat *seat; struct wl_registry *registry; struct ext_idle_notifier_v1 *notifier; uint32_t notifier_global_name; gboolean supports_input_idle; gboolean ready; GHashTable *watches; uint32_t next_id; }; static void notification_handle_idled (void *data, struct ext_idle_notification_v1 *notification) { FtWaylandIdleMonitorWatch *watch = data; (void) notification; if (watch->on_idled != NULL) { watch->on_idled (watch->id, watch->user_data); } } static void notification_handle_resumed (void *data, struct ext_idle_notification_v1 *notification) { FtWaylandIdleMonitorWatch *watch = data; (void) notification; if (watch->on_resumed != NULL) { watch->on_resumed (watch->id, watch->user_data); } } static const struct ext_idle_notification_v1_listener notification_listener = { .idled = notification_handle_idled, .resumed = notification_handle_resumed, }; static void registry_handle_global (void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) { FtWaylandIdleMonitor *monitor = data; (void) registry; if (strcmp (interface, ext_idle_notifier_v1_interface.name) != 0) { return; } if (monitor->notifier != NULL) { return; } uint32_t bind_version = version < FT_WAYLAND_IDLE_MONITOR_BIND_VERSION ? version : FT_WAYLAND_IDLE_MONITOR_BIND_VERSION; monitor->notifier = wl_registry_bind (registry, name, &ext_idle_notifier_v1_interface, bind_version); monitor->notifier_global_name = name; monitor->supports_input_idle = bind_version >= 2; monitor->ready = monitor->notifier != NULL; } static void registry_handle_global_remove (void *data, struct wl_registry *registry, uint32_t name) { FtWaylandIdleMonitor *monitor = data; (void) registry; if (monitor->notifier == NULL || monitor->notifier_global_name != name) { return; } ext_idle_notifier_v1_destroy (monitor->notifier); monitor->notifier = NULL; monitor->notifier_global_name = 0; monitor->supports_input_idle = FALSE; monitor->ready = FALSE; } static const struct wl_registry_listener registry_listener = { .global = registry_handle_global, .global_remove = registry_handle_global_remove, }; static void watch_free (gpointer data) { FtWaylandIdleMonitorWatch *watch = data; if (watch->notification != NULL) { ext_idle_notification_v1_destroy (watch->notification); watch->notification = NULL; } g_free (watch); } FtWaylandIdleMonitor * ft_wayland_idle_monitor_new (struct wl_display *display, struct wl_seat *seat) { g_return_val_if_fail (display != NULL, NULL); g_return_val_if_fail (seat != NULL, NULL); FtWaylandIdleMonitor *monitor = g_new0 (FtWaylandIdleMonitor, 1); monitor->display = display; monitor->seat = seat; monitor->watches = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, watch_free); monitor->registry = wl_display_get_registry (display); wl_registry_add_listener (monitor->registry, ®istry_listener, monitor); wl_display_flush (display); wl_display_dispatch_pending (display); if (wl_display_roundtrip (display) < 0) { g_warning ("Wayland: wl_display_roundtrip failed while binding ext_idle_notifier_v1"); } return monitor; } FtWaylandIdleMonitor * ft_wayland_idle_monitor_ref (FtWaylandIdleMonitor *monitor) { g_return_val_if_fail (monitor != NULL, NULL); return monitor; } void ft_wayland_idle_monitor_unref (FtWaylandIdleMonitor *monitor) { ft_wayland_idle_monitor_free (monitor); } void ft_wayland_idle_monitor_free (FtWaylandIdleMonitor *monitor) { if (monitor == NULL) { return; } if (monitor->watches != NULL) { g_hash_table_remove_all (monitor->watches); g_hash_table_unref (monitor->watches); monitor->watches = NULL; } if (monitor->notifier != NULL) { ext_idle_notifier_v1_destroy (monitor->notifier); monitor->notifier = NULL; monitor->notifier_global_name = 0; } if (monitor->registry != NULL) { wl_registry_destroy (monitor->registry); monitor->registry = NULL; } g_free (monitor); } gboolean ft_wayland_idle_monitor_is_ready (FtWaylandIdleMonitor *monitor) { g_return_val_if_fail (monitor != NULL, FALSE); return monitor->ready && monitor->notifier != NULL; } gboolean ft_wayland_idle_monitor_supports_input_idle (FtWaylandIdleMonitor *monitor) { g_return_val_if_fail (monitor != NULL, FALSE); return monitor->supports_input_idle; } static uint32_t add_notification_internal (FtWaylandIdleMonitor *monitor, uint32_t timeout_ms, FtWaylandIdleMonitorCallback on_idled, FtWaylandIdleMonitorCallback on_resumed, gpointer user_data, gboolean use_input_idle) { uint32_t id = monitor->next_id++; if (monitor->next_id == 0) { monitor->next_id = 1; } FtWaylandIdleMonitorWatch *watch = g_new0 (FtWaylandIdleMonitorWatch, 1); watch->id = id; watch->timeout_ms = timeout_ms; watch->on_idled = on_idled; watch->on_resumed = on_resumed; watch->user_data = user_data; if (use_input_idle) { watch->notification = ext_idle_notifier_v1_get_input_idle_notification (monitor->notifier, timeout_ms, monitor->seat); } else { watch->notification = ext_idle_notifier_v1_get_idle_notification (monitor->notifier, timeout_ms, monitor->seat); } ext_idle_notification_v1_add_listener (watch->notification, ¬ification_listener, watch); g_hash_table_insert (monitor->watches, GUINT_TO_POINTER (id), watch); return id; } uint32_t ft_wayland_idle_monitor_add_notification (FtWaylandIdleMonitor *monitor, uint32_t timeout_ms, FtWaylandIdleMonitorCallback on_idled, FtWaylandIdleMonitorCallback on_resumed, gpointer user_data) { g_return_val_if_fail (monitor != NULL, 0); g_return_val_if_fail (ft_wayland_idle_monitor_is_ready (monitor), 0); return add_notification_internal (monitor, timeout_ms, on_idled, on_resumed, user_data, FALSE); } uint32_t ft_wayland_idle_monitor_add_input_notification (FtWaylandIdleMonitor *monitor, uint32_t timeout_ms, FtWaylandIdleMonitorCallback on_idled, FtWaylandIdleMonitorCallback on_resumed, gpointer user_data) { g_return_val_if_fail (monitor != NULL, 0); g_return_val_if_fail (ft_wayland_idle_monitor_is_ready (monitor), 0); g_return_val_if_fail (monitor->supports_input_idle, 0); return add_notification_internal (monitor, timeout_ms, on_idled, on_resumed, user_data, TRUE); } void ft_wayland_idle_monitor_remove_notification (FtWaylandIdleMonitor *monitor, uint32_t id) { g_return_if_fail (monitor != NULL); if (id == 0) { return; } g_hash_table_remove (monitor->watches, GUINT_TO_POINTER (id)); } focustimerhq-FocusTimer-8581be2/src/plugins/wayland/meson.build000066400000000000000000000024641520625676500247360ustar00rootroot00000000000000if enable_plugin_wayland protocol_xml = files('protocols/ext-idle-notify-v1.xml') ext_idle_protocol_client_header = custom_target( 'ext-idle-protocol-client-header', input: protocol_xml, output: 'ext-idle-protocol-client.h', command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], ) ext_idle_protocol_private_code = custom_target( 'ext-idle-protocol-private-code', input: protocol_xml, output: 'ext-idle-protocol.c', command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], ) wayland_plugin_inc = include_directories('.') wayland_plugin_resources = gnome.compile_resources( 'wayland-plugin-resources', 'wayland.gresource.xml', c_name: 'wayland_plugin', ) libft_plugin_wayland = static_library( 'ft_plugin_wayland', files( 'idle-monitor.c', 'idle-monitor-provider.vala', 'wayland.vala', ) + [ ext_idle_protocol_client_header, ext_idle_protocol_private_code, ] + wayland_plugin_resources, dependencies: [ libft_ui_dep, gtk_wayland_dep, wayland_client_dep, ], vala_args: [ '--vapidir', meson.current_source_dir(), '--pkg', 'ft-wayland', ], include_directories: [config_h_dir, wayland_plugin_inc], ) libft_plugins += [libft_plugin_wayland] endif focustimerhq-FocusTimer-8581be2/src/plugins/wayland/protocols/000077500000000000000000000000001520625676500246125ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/wayland/protocols/ext-idle-notify-v1.xml000066400000000000000000000133711520625676500307060ustar00rootroot00000000000000 Copyright © 2015 Martin Gräßlin Copyright © 2022 Simon Ser 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 (including the next paragraph) 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. This interface allows clients to monitor user idle status. After binding to this global, clients can create ext_idle_notification_v1 objects to get notified when the user is idle for a given amount of time. Destroy the manager object. All objects created via this interface remain valid. Create a new idle notification object. The notification object has a minimum timeout duration and is tied to a seat. The client will be notified if the seat is inactive for at least the provided timeout. See ext_idle_notification_v1 for more details. A zero timeout is valid and means the client wants to be notified as soon as possible when the seat is inactive. Create a new idle notification object to track input from the user, such as keyboard and mouse movement. Because this object is meant to track user input alone, it ignores idle inhibitors. The notification object has a minimum timeout duration and is tied to a seat. The client will be notified if the seat is inactive for at least the provided timeout. See ext_idle_notification_v1 for more details. A zero timeout is valid and means the client wants to be notified as soon as possible when the seat is inactive. This interface is used by the compositor to send idle notification events to clients. Initially the notification object is not idle. The notification object becomes idle when no user activity has happened for at least the timeout duration, starting from the creation of the notification object. User activity may include input events or a presence sensor, but is compositor-specific. How this notification responds to idle inhibitors depends on how it was constructed. If constructed from the get_idle_notification request, then if an idle inhibitor is active (e.g. another client has created a zwp_idle_inhibitor_v1 on a visible surface), the compositor must not make the notification object idle. However, if constructed from the get_input_idle_notification request, then idle inhibitors are ignored, and only input from the user, e.g. from a keyboard or mouse, counts as activity. When the notification object becomes idle, an idled event is sent. When user activity starts again, the notification object stops being idle, a resumed event is sent and the timeout is restarted. Destroy the notification object. This event is sent when the notification object becomes idle. It's a compositor protocol error to send this event twice without a resumed event in-between. This event is sent when the notification object stops being idle. It's a compositor protocol error to send this event twice without an idled event in-between. It's a compositor protocol error to send this event prior to any idled event. focustimerhq-FocusTimer-8581be2/src/plugins/wayland/wayland.gresource.xml000066400000000000000000000002311520625676500267400ustar00rootroot00000000000000 wayland.plugin focustimerhq-FocusTimer-8581be2/src/plugins/wayland/wayland.plugin000066400000000000000000000001471520625676500254470ustar00rootroot00000000000000[Plugin] Name=Wayland Module=wayland Builtin=true Embedded=wayland_peas_register_types X-Priority=high focustimerhq-FocusTimer-8581be2/src/plugins/wayland/wayland.vala000066400000000000000000000006671520625676500251030ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Wayland { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; object_module.register_extension_type (typeof (Ft.IdleMonitorProvider), typeof (Wayland.IdleMonitorProvider)); } } focustimerhq-FocusTimer-8581be2/src/plugins/xfce/000077500000000000000000000000001520625676500220545ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/plugins/xfce/interfaces.vala000066400000000000000000000010561520625676500250460ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Xfce { /** * https://docs.xfce.org/apps/xfce4-screensaver/dbus */ [DBus (name = "org.xfce.ScreenSaver")] public interface ScreenSaver : GLib.Object { public abstract async bool get_active () throws GLib.DBusError, GLib.IOError; [DBus (no_reply = true)] public abstract async void @lock () throws GLib.DBusError, GLib.IOError; public signal void active_changed (bool new_value); } } focustimerhq-FocusTimer-8581be2/src/plugins/xfce/meson.build000066400000000000000000000007411520625676500242200ustar00rootroot00000000000000if get_option('plugin_xfce').enabled() xfce_plugin_resources = gnome.compile_resources( 'xfce-plugin-resources', 'xfce.gresource.xml', c_name: 'xfce_plugin', ) libft_plugin_xfce = static_library( 'ft_plugin_xfce', files( 'xfce.vala', 'interfaces.vala', 'screen-saver-provider.vala', ) + xfce_plugin_resources, dependencies: [libft_ui_dep], include_directories: config_h_dir, ) libft_plugins += [libft_plugin_xfce] endiffocustimerhq-FocusTimer-8581be2/src/plugins/xfce/screen-saver-provider.vala000066400000000000000000000063351520625676500271550ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Xfce { public class ScreenSaverProvider : Ft.Provider, Ft.ScreenSaverProvider { public bool active { get { return this._active; } } private bool _active = false; private Xfce.ScreenSaver? screensaver_proxy = null; private uint watcher_id = 0; private ulong active_changed_id = 0; private void on_name_appeared (GLib.DBusConnection connection, string name, string name_owner) { this.available = true; } private void on_name_vanished (GLib.DBusConnection? connection, string name) { this.available = false; } private void on_active_changed (bool new_value) { if (this._active != new_value) { this._active = new_value; this.notify_property ("active"); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.watcher_id = GLib.Bus.watch_name (GLib.BusType.SESSION, "org.xfce.ScreenSaver", GLib.BusNameWatcherFlags.NONE, this.on_name_appeared, this.on_name_vanished); } public override async void uninitialize () throws GLib.Error { if (this.watcher_id != 0) { GLib.Bus.unwatch_name (this.watcher_id); this.watcher_id = 0; } } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { try { this.screensaver_proxy = yield GLib.Bus.get_proxy (GLib.BusType.SESSION, "org.xfce.ScreenSaver", "/org/xfce/ScreenSaver", GLib.DBusProxyFlags.DO_NOT_AUTO_START, cancellable); this._active = yield this.screensaver_proxy.get_active (); this.active_changed_id = this.screensaver_proxy.active_changed.connect (this.on_active_changed); } catch (GLib.Error error) { GLib.warning ("Error while initializing session proxy: %s", error.message); } } public override async void disable () throws GLib.Error { if (this.active_changed_id != 0) { this.screensaver_proxy.disconnect (this.active_changed_id); this.active_changed_id = 0; } this.screensaver_proxy = null; if (this._active) { this._active = false; this.notify_property ("active"); } } } } focustimerhq-FocusTimer-8581be2/src/plugins/xfce/xfce.gresource.xml000066400000000000000000000002231520625676500255150ustar00rootroot00000000000000 xfce.plugin focustimerhq-FocusTimer-8581be2/src/plugins/xfce/xfce.plugin000066400000000000000000000001361520625676500242210ustar00rootroot00000000000000[Plugin] Name=XFCE Module=xfce Builtin=true Embedded=xfce_peas_register_types X-Priority=high focustimerhq-FocusTimer-8581be2/src/plugins/xfce/xfce.vala000066400000000000000000000010001520625676500236350ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Xfce { [ModuleInit] public void peas_register_types (GLib.TypeModule module) { var object_module = module as Peas.ObjectModule; if (Ft.get_desktop_name () != "xfce") { return; } object_module.register_extension_type (typeof (Ft.ScreenSaverProvider), typeof (Xfce.ScreenSaverProvider)); } } focustimerhq-FocusTimer-8581be2/src/ui/000077500000000000000000000000001520625676500200635ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/interfaces.vala000066400000000000000000000017001520625676500230510ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * * This module holds public interfaces for Peas extensions. */ namespace Ft { public abstract class ApplicationExtension : GLib.Object { } public abstract class WindowExtension : GLib.Object { public Ft.Window? window { owned get { return this.window_ref.@get () as Ft.Window; } set { this.window_ref.@set (value); } } private GLib.WeakRef window_ref; } public abstract class PreferencesWindowExtension : GLib.Object { public Ft.PreferencesWindow? window { owned get { return this.window_ref.@get () as Ft.PreferencesWindow; } set { this.window_ref.@set (value); } } private GLib.WeakRef window_ref; } } focustimerhq-FocusTimer-8581be2/src/ui/log/000077500000000000000000000000001520625676500206445ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/log/log-window.ui000066400000000000000000000406531520625676500233010ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/log/log-window.vala000066400000000000000000000324021520625676500236000ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { private string get_event_icon_name (string event_name) { switch (event_name) { case "start": return "timer-start-symbolic"; case "stop": return "timer-stop-symbolic"; case "skip": return "timer-skip-symbolic"; case "rewind": return "timer-rewind-symbolic"; case "pause": return "timer-pause-symbolic"; case "resume": return "timer-start-symbolic"; case "reset": return "timer-reset-symbolic"; default: return "event-symbolic"; } } private string get_action_icon_name (string event_name) { switch (event_name) { case "triggered": return "custom-action-symbolic"; case "entered-condition": return "condition-enter-symbolic"; case "exited-condition": return "condition-exit-symbolic"; default: return "custom-action-symbolic"; } } /** * Custom model for handling sections (time headers). */ private class SectionedLogModel : GLib.Object, GLib.ListModel, Gtk.SectionModel { public GLib.ListModel? model { get { return this._model; } construct { this._model = value; this._model.items_changed.connect (this.on_items_changed); } } private GLib.ListModel? _model = null; public SectionedLogModel (GLib.ListModel model) { GLib.Object ( model: model ); } /* * GLib.ListModel interface */ public GLib.Object? get_item (uint position) { return this._model.get_item (position); } public GLib.Type get_item_type () { return this._model.get_item_type (); } public uint get_n_items () { return this._model.get_n_items (); } private void on_items_changed (uint position, uint removed, uint added) { this.items_changed (position, removed, added); if (added > 0) { this.sections_changed (position, added); } } /* * Gtk.SectionModel interface */ private static inline int64 calculate_hash (int64 timestamp) { return timestamp / Ft.Interval.MINUTE; } public void get_section (uint position, out uint out_start, out uint out_end) { var reference_item = (Ft.LogEntry) this._model.get_item (position); var reference_hash = calculate_hash (reference_item.timestamp); var n_items = this._model.get_n_items (); out_start = position; out_end = position + 1; while (out_start > 0) { var item = (Ft.LogEntry) this._model.get_item (out_start - 1); if (calculate_hash (item.timestamp) == reference_hash) { out_start--; } else { break; } } while (out_end < n_items) { var item = (Ft.LogEntry) this._model.get_item (out_end); if (calculate_hash (item.timestamp) == reference_hash) { out_end++; } else { break; } } } public override void dispose () { if (this._model != null) { this._model.items_changed.disconnect (this.on_items_changed); this._model = null; } base.dispose (); } } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/log/log-window.ui")] public class LogWindow : Adw.ApplicationWindow { [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.ListView listview; [GtkChild] private unowned Gtk.Label header_label; [GtkChild] private unowned Gtk.Label datetime_label; [GtkChild] private unowned Gtk.Label context_label; [GtkChild] private unowned Gtk.Label command_line_header_label; [GtkChild] private unowned Gtk.Label command_line_label; [GtkChild] private unowned Gtk.Label command_error_header_label; [GtkChild] private unowned Gtk.Label command_error_message_label; [GtkChild] private unowned Gtk.Label command_output_header_label; [GtkChild] private unowned Gtk.Label command_output_label; [GtkChild] private unowned Gtk.Box command_exit_code_box; [GtkChild] private unowned Gtk.Label command_exit_code_label; [GtkChild] private unowned Gtk.Label command_execution_time_label; private Ft.Logger logger; private Ft.LogEntry? selected_entry; private uint update_contents_id = 0; construct { this.logger = new Ft.Logger (); var model = new Gtk.SingleSelection (new SectionedLogModel (this.logger.model)); model.autoselect = true; model.can_unselect = false; model.selection_changed.connect (this.on_selection_changed); model.items_changed.connect (this.on_items_changed); this.listview.model = model; this.update_contents_id = 0; this.update_stack_visible_child (); this.update_contents (); } private void update_contents () { if (this.update_contents_id != 0) { this.remove_tick_callback (this.update_contents_id); this.update_contents_id = 0; } if (this.selected_entry != null) { this.selected_entry.notify.disconnect (this.on_selected_entry_notify); this.selected_entry = null; } var model = (Gtk.SingleSelection?) this.listview.model; var entry = (Ft.LogEntry?) model?.selected_item; if (entry != null) { var datetime = new GLib.DateTime.from_unix_utc (entry.timestamp / Ft.Interval.SECOND); this.header_label.label = entry.label; this.datetime_label.label = datetime != null ? datetime.to_local ().format ("%c") : ""; this.context_label.label = entry.context != null ? entry.context.to_json () : ""; this.command_line_header_label.visible = false; this.command_line_label.visible = false; this.command_error_header_label.visible = false; this.command_error_message_label.visible = false; this.command_output_header_label.visible = false; this.command_output_label.visible = false; this.command_exit_code_box.visible = false; if (entry is Ft.ActionLogEntry) { var action_entry = (Ft.ActionLogEntry) entry; if (action_entry.command_line != "") { this.command_line_label.label = action_entry.command_line; this.command_line_header_label.visible = true; this.command_line_label.visible = true; } if (action_entry.command_error_message != null && action_entry.command_error_message != "") { this.command_error_message_label.label = action_entry.command_error_message; this.command_error_header_label.visible = true; this.command_error_message_label.visible = true; } if (action_entry.command_output != null && action_entry.command_output != "") { this.command_output_label.label = action_entry.command_output.strip (); this.command_output_header_label.visible = true; this.command_output_label.visible = true; } if (action_entry.command_exit_code >= 0) { this.command_exit_code_box.visible = true; this.command_exit_code_label.label = action_entry.command_exit_code.to_string (); this.command_execution_time_label.label = "%u ms".printf ( Ft.Timestamp.to_milliseconds_uint (action_entry.command_execution_time)); } } entry.notify.connect (this.on_selected_entry_notify); } this.selected_entry = entry; } private void queue_update_contents () { if (this.update_contents_id == 0) { this.update_contents_id = this.add_tick_callback (() => { this.update_contents_id = 0; this.update_contents (); return GLib.Source.REMOVE; }); } } private void update_stack_visible_child () { var model = (Gtk.SingleSelection) this.listview.model; var n_items = model != null ? model.get_n_items () : 0U; var visible_child_name = n_items == 0U ? "placeholder" : "content"; if (this.stack.visible_child_name != visible_child_name) { if (visible_child_name == "content") { this.update_contents (); } this.stack.visible_child_name = visible_child_name; } } private void on_items_changed (GLib.ListModel model, uint position, uint removed, uint added) { this.update_stack_visible_child (); } private void on_selection_changed (uint position, uint n_items) { this.update_contents (); } private void on_selected_entry_notify () { this.queue_update_contents (); } [GtkCallback] private void setup_list_item (GLib.Object object) { var list_item = (Gtk.ListItem) object; list_item.child = new Ft.SidebarRow (); } [GtkCallback] private void bind_list_item (GLib.Object object) { var list_item = (Gtk.ListItem) object; var entry = (Ft.LogEntry) list_item.item; var row = (Ft.SidebarRow?) list_item.child; if (row == null) { return; } if (entry is Ft.EventLogEntry) { var event_entry = (Ft.EventLogEntry) entry; row.icon_name = get_event_icon_name (event_entry.event_name); } if (entry is Ft.ActionLogEntry) { var action_entry = (Ft.ActionLogEntry) entry; row.icon_name = get_action_icon_name (action_entry.event_name); if (action_entry.command_error_message != null) { row.parent.add_css_class ("error"); } else { action_entry.notify["command-error-message"].connect ( () => { row.parent.add_css_class ("error"); }); } if (action_entry.command_line != "" && action_entry.command_exit_code < 0 && action_entry.command_error_message == null) { var spinner = new Gtk.Spinner (); spinner.start (); row.suffix = spinner; action_entry.notify["command-exit-code"].connect ( (object, pspec) => { spinner.stop (); }); } } row.title = entry.label; } public void select (ulong entry_id) { var model = (Gtk.SingleSelection?) this.listview.model; var n_items = model.n_items; for (var position = 0; position < n_items; position++) { var entry = (Ft.LogEntry) model.get_item (position); if (entry.id == entry_id) { model.set_selected (position); break; } } } public override void dispose () { if (this.listview != null) { this.listview.model.selection_changed.disconnect (this.on_selection_changed); this.listview.model.items_changed.disconnect (this.on_items_changed); } this.logger = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/log/widgets/000077500000000000000000000000001520625676500223125ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/log/widgets/time-label.vala000066400000000000000000000103571520625676500252000ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public sealed class TimeLabel : Gtk.Widget { public int64 timestamp { get { return this._timestamp; } set { this._timestamp = value; this.update_label (); } } public float xalign { get { return this.label.xalign; } set { this.label.xalign = value; } } private int64 _timestamp = Ft.Timestamp.UNDEFINED; private Gtk.Label? label; construct { this.label = new Gtk.Label (null); this.label.ellipsize = Pango.EllipsizeMode.END; this.label.single_line_mode = true; this.label.wrap = false; this.label.set_parent (this); } private string format_timestamp () { if (this._timestamp < 0) { return ""; } var seconds = this._timestamp / Ft.Interval.SECOND; var datetime = (new GLib.DateTime.from_unix_utc (seconds)).to_local (); // TODO: include days ago / relative time return datetime.format ("%H:%M"); } private void update_label (int64 timestamp = Ft.Timestamp.UNDEFINED) { this.label.label = this.format_timestamp (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } /** * Estimate size. * * Interpolate between two children and with-hours / without-hours. */ public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { this.label.measure (orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } public override void size_allocate (int width, int height, int baseline) { var allocation = Gtk.Allocation (); this.label.measure ( Gtk.Orientation.VERTICAL, height, null, out allocation.height, null, null); this.label.measure ( Gtk.Orientation.HORIZONTAL, -1, null, out allocation.width, null, null); switch (this.halign) { case Gtk.Align.START: allocation.x = 0; break; case Gtk.Align.END: allocation.x = width - allocation.width; break; case Gtk.Align.CENTER: case Gtk.Align.FILL: allocation.x = (width - allocation.width) / 2; break; default: assert_not_reached (); } allocation.y = (height - allocation.height) / 2; this.label.allocate_size (allocation, baseline); } public override void snapshot (Gtk.Snapshot snapshot) { this.snapshot_child (this.label, snapshot); } public override void dispose () { if (this.label != null) { this.label.unparent (); this.label = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/000077500000000000000000000000001520625676500210075ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/dialogs/000077500000000000000000000000001520625676500224315ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/dialogs/about-dialog.vala000066400000000000000000000025101520625676500256430ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public Adw.AboutDialog create_about_dialog () { var about_dialog = new Adw.AboutDialog (); about_dialog.application_icon = Config.APPLICATION_ID; about_dialog.application_name = GLib.Environment.get_application_name (); about_dialog.version = Config.PACKAGE_VERSION; about_dialog.website = Config.PACKAGE_WEBSITE; about_dialog.issue_url = Config.PACKAGE_ISSUE_URL; about_dialog.support_url = Config.PACKAGE_SUPPORT_URL; about_dialog.developer_name = "Kamil Prusko"; about_dialog.developers = { "Kamil Prusko ", "Arun Mahapatra " }; about_dialog.copyright = "\xc2\xa9 2011-2026 Arun Mahapatra, Kamil Prusko"; about_dialog.license_type = Gtk.License.GPL_3_0; // translators: Replace this string with your names, one name per line. var translator_credits = _("translator-credits"); if (translator_credits != "translator-credits") { about_dialog.translator_credits = translator_credits; } about_dialog.add_link (_("Donate"), Config.PACKAGE_DONATE_URL); return about_dialog; } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/000077500000000000000000000000001520625676500221455ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/000077500000000000000000000000001520625676500234315ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/bar-chart.vala000066400000000000000000000724451520625676500261550ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * Widget for displaying a bar chart or a histogram. */ public class BarChart : Ft.Chart { private const int MIN_BAR_WIDTH = 16; private const int MAX_BAR_WIDTH = 30; private const float BAR_SPACING = 0.25f; private const int MIN_BAR_SPACING = 2; private const float ASPECT_RATIO = 1.66f; private const float BAR_HIT_ZONE_PADDING = 10.0f; private const double EPSILON = 0.00001; private const double DEFAULT_VALUE = 0.0; public bool stacked { get { return this._stacked; } set { if (this._stacked == value) { return; } this._stacked = value; this.queue_allocate (); } } public bool show_empty_bars { get { return this._show_empty_bars; } set { if (this._show_empty_bars == value) { return; } this._show_empty_bars = value; this.queue_draw (); } } public bool activate_on_click { get; set; default = false; } public float reference_value { get { return this.contents.y_value_to; } set { this.contents.y_value_to = value; } } private bool _stacked = true; private bool _show_empty_bars = false; private double transform_slope = 1.0; private double transform_intercept = 0.0; private Bucket[] buckets; private Category[] categories; private Ft.Matrix? data; private int bar_width; private int bar_height; private int bar_radius; private int tooltip_bar_index = -1; private Gtk.Widget? tooltip_widget; construct { this.buckets = {}; this.categories = {}; this.contents.grid.vertical = false; this.contents.x_axis.continous = false; this.contents.y_value_from = 0.0f; this.contents.y_value_to = float.NAN; } /* * Data */ private double calculate_bucket_sum (uint bucket_index) { if (this.data == null) { return 0.0; } var values = this.data.get_vector (0, (int) bucket_index); for (var category_index = 0; category_index < this.categories.length; category_index++) { if (!this.categories[category_index].visible) { values.@set (category_index, 0.0); } } return values != null ? values.sum () : 0.0; } private double calculate_bucket_max (uint bucket_index) { if (this.data == null) { return 0.0; } var values = this.data.get_vector (0, (int) bucket_index); for (var category_index = 0; category_index < this.categories.length; category_index++) { if (!this.categories[category_index].visible) { values.@set (category_index, 0.0); } } return values != null ? values.max () : 0.0; } private inline double calculate_bucket_total_value (uint bucket_index) { return this._stacked ? this.calculate_bucket_sum (bucket_index) : this.calculate_bucket_max (bucket_index); } private void ensure_categories (uint count) { var previous_count = this.categories.length; if (previous_count >= count) { return; } this.categories.resize ((int) count); for (var index = previous_count; index < count; index++) { this.categories[index] = Category () { label = "", color = this.get_color (), visible = true }; } this.queue_update (); } private void ensure_buckets (uint count) { var previous_count = this.buckets.length; if (previous_count >= count) { return; } this.buckets.resize ((int) count); for (var index = previous_count; index < count; index++) { this.buckets[index] = Bucket () { label = "" }; } this.queue_update (); } private inline void ensure_data () { if (this.data == null) { var bucket_count = this.buckets.length; var category_count = this.categories.length; this.data = new Ft.Matrix (bucket_count, category_count); } } private inline void grow_data (uint bucket_count, uint category_count) requires (bucket_count < 1000) { this.ensure_data (); bucket_count = uint.max (this.data.shape[0], bucket_count); category_count = uint.max (this.data.shape[1], category_count); if (this.data.shape[0] != bucket_count || this.data.shape[1] != category_count) { this.data.resize (bucket_count, category_count, DEFAULT_VALUE); } } private bool is_bucket_empty (uint bucket_index) { var category_count = int.min ((int) this.data.shape[1], this.categories.length); var total_abs_value = 0.0; for (var category_index = 0; category_index < category_count; category_index++) { var category_value = this.get_value (bucket_index, category_index); if (category_value.is_finite ()) { total_abs_value += category_value.abs (); } } return total_abs_value.abs () <= EPSILON; // XXX: use Use CanvasLayoutChild.range.size.height ? } public uint get_bars_count () { return this.buckets.length; } public uint get_categories_count () { return this.categories.length; } public double get_category_total (uint category_index) { var total = this.data.get_vector (-1, (int) category_index).sum (); if (this._stacked) { for (var index = 0; index < category_index; index++) { total += this.data.get_vector (-1, index).sum (); } } return total; } public void remove_all_bars () { this.buckets = null; } public void set_bar_label (uint bar_index, string label, string tooltip_label = "") { if (tooltip_label == "") { tooltip_label = label; } this.ensure_buckets (bar_index + 1); this.buckets[bar_index].label = label; this.buckets[bar_index].tooltip_label = tooltip_label; // this.queue_allocate (); // this.contents.x_axis.queue_allocate (); } public void set_category_label (uint category_index, string label) { this.ensure_categories (category_index + 1); this.categories[category_index].label = label; } public void set_category_color (uint category_index, Gdk.RGBA color) { this.ensure_categories (category_index + 1); this.categories[category_index].color = color; this.queue_draw (); } public void set_category_unit (uint category_index, Ft.Unit unit) { this.ensure_categories (category_index + 1); this.categories[category_index].unit = unit; } public void set_category_visible (uint category_index, bool visible) { this.ensure_categories (category_index + 1); this.categories[category_index].visible = visible; this.queue_draw (); } public void fill (double value) { this.grow_data ((uint) this.buckets.length, (uint) this.categories.length); this.data.fill (value); this.queue_update (); } public void set_values (uint bar_index, double[] values) { this.grow_data (bar_index + 1, values.length); if (values.length > this.data.shape[1]) { GLib.warning ("BarChart.set_values received a vector with length %d when there are only %u categories", values.length, this.data.shape[1]); } var category_index = 0; for (; category_index < values.length; category_index++) { this.data.@set ((int) bar_index, category_index, values[category_index]); } for (; category_index < this.data.shape[1]; category_index++) { this.data.@set ((int) bar_index, category_index, 0.0); } // XXX: update only changed bars this.queue_update (); } public double get_value (uint bar_index, uint category_index) { return this.data != null ? this.data.@get ((int) bar_index, (int) category_index, DEFAULT_VALUE) : DEFAULT_VALUE; } public void set_value (uint bar_index, uint category_index, double value) { this.grow_data (bar_index + 1, category_index + 1); this.data.@set ((int) bar_index, (int) category_index, value); if (category_index < this.categories.length && this.categories[category_index].visible) { // TODO: update only changed bars this.queue_update (); } } public inline void add_value (uint bar_index, uint category_index, double value) { this.set_value (bar_index, category_index, value + this.get_value (bar_index, category_index)); } /* * Bars */ private bool on_query_tooltip (Gtk.Widget widget, int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { var bar_index = widget.get_data ("index"); if (this.tooltip_bar_index != bar_index) { this.tooltip_bar_index = (int) bar_index; this.tooltip_widget = this.create_tooltip_widget (bar_index); } tooltip.set_custom (this.tooltip_widget); return this.tooltip_widget != null; } private Gtk.Widget? create_tooltip_widget (uint bar_index) { var bucket = this.buckets[bar_index]; var category_count = this.categories.length; var grid = new Gtk.Grid (); grid.column_spacing = 10; grid.row_spacing = 5; grid.row_homogeneous = true; grid.add_css_class ("tooltip-contents"); var header_label = new Gtk.Label (bucket.tooltip_label); header_label.add_css_class ("tooltip-header"); grid.attach (header_label, 0, 0, 2, 1); for (var category_index = category_count - 1; category_index >= 0; category_index--) { var category = this.categories[category_index]; var category_value = this.get_value (bar_index, category_index); var category_label = new Gtk.Label (@"$(category.label):"); category_label.halign = Gtk.Align.START; grid.attach (category_label, 0, 1 + category_index); var value_label = new Gtk.Label ( this.format_tooltip_value (category_index, category_value)); value_label.halign = Gtk.Align.END; grid.attach (value_label, 1, 1 + category_index); } return grid; } private Gtk.Widget create_bar () { var bar = new Ft.Gizmo (Ft.BarChart.measure_bar_cb, null, Ft.BarChart.snapshot_bar_cb, Ft.BarChart.contains_bar_cb, null, null); bar.focusable = false; bar.has_tooltip = true; bar.add_css_class ("bar"); bar.query_tooltip.connect (Ft.BarChart.on_query_tooltip_cb); if (this.activate_on_click) { unowned var weak_bar = bar; var click_gesture = new Gtk.GestureClick (); click_gesture.set_button (Gdk.BUTTON_PRIMARY); click_gesture.released.connect ((n_press, x, y) => { BarChart.on_clicked_cb (weak_bar); }); bar.add_controller (click_gesture); } else { bar.set_state_flags (Gtk.StateFlags.INSENSITIVE, false); } return bar; } private static Ft.BarChart? from_gizmo (Ft.Gizmo gizmo) { Gtk.Widget? widget = gizmo; while (widget != null) { var chart = widget as Ft.BarChart; if (chart != null) { return chart; } widget = widget.get_parent (); } return null; } private static Ft.BarChart? from_widget (Gtk.Widget widget) { Gtk.Widget? current = widget; while (current != null) { var chart = current as Ft.BarChart; if (chart != null) { return chart; } current = current.get_parent (); } return null; } private static void measure_bar_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var self = BarChart.from_gizmo (gizmo); if (self != null) { self.measure_bar (gizmo, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } } private static void snapshot_bar_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = BarChart.from_gizmo (gizmo); if (self != null) { self.snapshot_bar (gizmo, snapshot); } } private static bool contains_bar_cb (Ft.Gizmo gizmo, double x, double y) { var self = BarChart.from_gizmo (gizmo); return self != null ? self.contains_bar (gizmo, x, y) : false; } private static bool on_query_tooltip_cb (Gtk.Widget widget, int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { var self = BarChart.from_widget (widget); return self != null ? self.on_query_tooltip (widget, x, y, keyboard_tooltip, tooltip) : false; } private static void on_clicked_cb (Gtk.Widget widget) { var self = BarChart.from_gizmo ((Ft.Gizmo) widget); var bar_index = widget.get_data ("index"); if (self != null) { self.bar_activated (bar_index); } } private inline void update_bar (Gtk.Widget bar, uint bar_index) { bar.set_data ("index", bar_index); var layout_child = this.contents.canvas.get_layout_child (bar); if (layout_child != null) { var total_value = this.calculate_bucket_total_value (bar_index); var bar_x = (double) bar_index * this.transform_slope + this.transform_intercept; layout_child.x = (float) bar_x; layout_child.y = 0.0f; layout_child.set_range ((float) bar_x, (float) bar_x, 0.0f, (float) total_value); } } private void measure_bar (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = orientation == Gtk.Orientation.HORIZONTAL ? this.bar_width : this.bar_height; natural = minimum; minimum_baseline = -1; natural_baseline = -1; } private void snapshot_bar (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var bar_index = gizmo.get_data ("index"); if (bar_index >= this.data.shape[0]) { GLib.warning ("Unable to snapshot bar %u: No data", bar_index); return; } if (!this.show_empty_bars && this.is_bucket_empty (bar_index)) { return; } var category_count = int.min ((int) this.data.shape[1], this.categories.length); var base_value = 0.0; var bar_width = (float) this.bar_width; var bar_height = (float) this.bar_height; var y_scale = (double) this.contents.canvas.y_scale; var is_shown = false; if (this._stacked) { for (var category_index = 0; category_index < category_count; category_index++) { var category_value = this.get_value (bar_index, category_index); if (category_value.is_finite ()) { base_value += category_value; } } } for (var category_index = category_count - 1; category_index >= 0; category_index--) { var category_value = this.get_value (bar_index, category_index); var category_color = this.categories[category_index].color; if (!category_value.is_finite ()) { continue; } if (!this._stacked) { base_value = category_value; } if (category_value * y_scale <= 0.0 && (category_index != 0 || is_shown)) { base_value -= category_value; continue; } var bounds = Graphene.Rect (); bounds.init (0.0f, bar_height - bar_width - (float)(base_value * y_scale), bar_width, (float)(category_value * y_scale) + bar_width); var outline = Gsk.RoundedRect (); outline.init_from_rect (bounds, bar_width / 2.0f); snapshot.push_rounded_clip (outline); snapshot.append_color (category_color, bounds); snapshot.pop (); base_value -= category_value; is_shown = true; } } private bool contains_bar (Ft.Gizmo gizmo, double x, double y) { if (!gizmo.get_mapped ()) { return false; } // Check if cursor fits within bar boundaries. var point = Graphene.Point () { x = (float) x, y = (float) y }; Graphene.Rect bounds; if (!gizmo.compute_bounds (gizmo, out bounds) || !bounds.contains_point (point)) { return false; } // Check if cursor fits within value bars and the ornaments (bar radius) var bar_index = gizmo.get_data ("index"); if (!this.show_empty_bars && this.is_bucket_empty (bar_index)) { return false; } var total_value = (float) this.calculate_bucket_total_value (bar_index); point.y += this.bar_radius + BAR_HIT_ZONE_PADDING; point = this.contents.canvas.transform_point (point); return point.y <= total_value; } private inline void foreach_bar (GLib.Func func) { var child = this.contents.canvas.get_first_child (); while (child != null) { var next_child = child.get_next_sibling (); func (child); child = next_child; } } /* * Chart */ /** * Charts `width` depends more on content and can be scrolled horizontally. * `height` is determined by the `Chart` using aspect-ratio. */ public override Gtk.SizeRequestMode get_contents_request_mode (Ft.Canvas canvas) { return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT; } public override void update_canvas (Ft.Canvas canvas) { var bars_count = this.get_bars_count (); var bar_index = 0U; // Update existing bars and remove unnecessary ones this.foreach_bar ( (bar) => { if (bar_index < bars_count) { this.update_bar (bar, bar_index); bar_index++; } else { canvas.remove_child (bar); } }); // Create missing bars while (bar_index < bars_count) { var bar = this.create_bar (); canvas.add_child (bar); this.update_bar (bar, bar_index); bar_index++; } // HACK: Sync x-axis synced with scale this.x_spacing = (float) this.transform_slope; } public override void measure_canvas (Ft.Canvas canvas, Gtk.Orientation orientation, int for_size, out int minimum, out int natural) { if (orientation == Gtk.Orientation.HORIZONTAL) { var bars_count = (int) this.buckets.length; var label_width = this.contents.x_axis.label_width; var min_margins = int.max (label_width - MIN_BAR_WIDTH, 0); var max_margins = int.max (label_width - MAX_BAR_WIDTH, 0); // Estimate content size var max_bar_spacing = int.max ( (int) Math.floorf ((float) MAX_BAR_WIDTH * BAR_SPACING), MIN_BAR_SPACING); var min_width = (MIN_BAR_WIDTH + MIN_BAR_SPACING) * bars_count - MIN_BAR_SPACING + min_margins; var max_width = (MAX_BAR_WIDTH + max_bar_spacing) * bars_count - max_bar_spacing + max_margins; var nat_width = (int) Math.roundf (ASPECT_RATIO * (float) max_width); // Calculate optimal bar size and spacing var segment_width = bars_count != 0 ? nat_width / bars_count : 0; var bar_spacing = int.max ((int) Math.floorf (BAR_SPACING * (float) segment_width), MIN_BAR_SPACING); var bar_width = int.min (segment_width - bar_spacing, MAX_BAR_WIDTH); minimum = min_width; natural = bars_count * (bar_width + bar_spacing) - bar_spacing; } else { minimum = this.height_request; natural = minimum; } } public override void measure_working_area (Ft.Canvas canvas, int available_width, int available_height, out Gdk.Rectangle working_area) { // Calculate optimal bar size and spacing var bars_count = (int) this.get_bars_count (); var segment_width = bars_count != 0 ? available_width / bars_count : 0; var bar_spacing = int.max ((int) Math.floorf (BAR_SPACING * (float) segment_width), MIN_BAR_SPACING); var bar_width = int.min (segment_width - bar_spacing, MAX_BAR_WIDTH); var bar_height = available_height; var bar_radius = bar_width / 2; working_area = Gdk.Rectangle () { x = bar_width / 2, y = bar_width - bar_radius, width = segment_width * (bars_count - 1), height = bar_height - 2 * bar_radius }; // Store measurements for bar allocation / snapshot this.bar_width = bar_width; this.bar_height = bar_height; this.bar_radius = bar_radius; this.foreach_bar ( (bar) => { var layout_child = canvas.get_layout_child (bar); if (layout_child != null) { layout_child.x_origin = this.bar_radius; layout_child.y_origin = this.bar_height - this.bar_radius; } }); } /* * Axes */ /** * Modify bars `x` position to concrete values rather than bar indices. Used for zooming. */ public void set_transform (double slope, double intercept) { this.transform_slope = slope; this.transform_intercept = intercept; } public override string format_x_value (double value) { var bar_index = (int) Math.round ((value - this.transform_intercept) / this.transform_slope); return bar_index >= 0 && bar_index < this.buckets.length ? this.buckets[bar_index].label : "∅"; // for debugging } private string format_tooltip_value (uint category_index, double value) { return category_index < this.categories.length ? this.categories[category_index].unit.format (value) : "%.2f".printf (value); } /* * Widget */ public override void dispose () { if (this.tooltip_widget != null) { this.tooltip_widget.unparent (); this.tooltip_widget = null; } this.data = null; this.categories = null; this.buckets = null; this.tooltip_widget = null; base.dispose (); } public signal void bar_activated (uint bar_index); } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/bubble-chart.ui000066400000000000000000000035151520625676500263260ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/bubble-chart.vala000066400000000000000000001062121520625676500266320ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/charts/bubble-chart.ui")] public class BubbleChart : Gtk.Widget { private const int MIN_BUBBLE_SIZE = 16; private const int MAX_BUBBLE_SIZE = 42; private const float BUBBLE_SPACING = 0.2f; private const float MIN_BUBBLE_RADIUS = 0.01f; private const float MIN_BUBBLE_VALUE = 60.0f; private const float BASE_RADIUS = 4.0f; private const double DEFAULT_VALUE = 0.0; /** * Value that should be visible on the chart. */ public double reference_value { get { return this._reference_value; } set { if (this._reference_value == value) { return; } this._reference_value = value; this.invalidate_max_value (); } } public uint category { get { return this._category; } set { if (this._category == value) { return; } this._category = value; this.invalidate_max_value (); } } public bool activate_on_click { get; set; default = false; } public uint levels { get { return this._levels; } set { if (this._levels == value) { return; } this._levels = value; this.update_level_radii (); this.queue_draw_bubbles (); } } [GtkChild] private unowned Gtk.Grid layout_grid; [GtkChild] private unowned Gtk.Box columns_box; [GtkChild] private unowned Gtk.Box rows_box; [GtkChild] private unowned Gtk.Grid bubbles_grid; private double _reference_value = 1.0; private uint _category = 0U; private uint _levels = 4U; private Category[] categories; private Bucket[,] buckets; private Ft.Matrix3D? data; private double max_value = 0.0; private int bubble_size; private float[] level_radii; private int tooltip_row = -1; private int tooltip_column = -1; private Gtk.Widget? tooltip_widget; private string[] rows_labels; private string[] columns_labels; private uint update_idle_id = 0; static construct { set_css_name ("chart"); } construct { this.categories = {}; this.rows_labels = {}; this.columns_labels = {}; this.level_radii = {}; } private inline string format_tooltip_value (uint category_index, double value) { return category_index < this.categories.length ? this.categories[category_index].unit.format (value) : "%.2f".printf (value); } private inline double calculate_max_value () { var category_data = this.data?.get_matrix (2, (int) this.category); return category_data != null ? double.max (category_data.max (), this._reference_value) : this._reference_value; } private inline float calculate_bubble_radius (double bubble_value) { var levels = this.level_radii.length; float bubble_radius; if (this.max_value.is_nan () || this.max_value <= 0.0) { return 0.0f; } if (levels <= 1) { // Exact value without quantization bubble_radius = (1.0f - BUBBLE_SPACING) * (float) this.bubble_size * 0.5f * (float) Math.sqrt (bubble_value / this.max_value); if (bubble_radius < MIN_BUBBLE_RADIUS) { bubble_radius = 0.0f; } } else if (bubble_value < MIN_BUBBLE_VALUE) { bubble_radius = this.level_radii[0]; } else { var level = 1 + ((int) Math.floor ( (double) (levels - 1) * (bubble_value / this.max_value))).clamp (0, levels - 2); bubble_radius = this.level_radii[level]; } return bubble_radius; } private void update_level_radii () { if (this._levels == 0) { if (this.level_radii.length > 0) { this.level_radii.resize (0); } return; } if (this.bubble_size <= 0) { return; } var levels = (int) this._levels + 1; // add base level for empty values var min_radius = (double) BASE_RADIUS; var max_radius = (1.0 - (double) BUBBLE_SPACING) * (double) this.bubble_size * 0.5; if (this.level_radii.length != levels) { this.level_radii.resize (levels); } for (var level = 0; level < levels; level++) { var t = (double) level / (double)(levels - 1); this.level_radii[level] = (float) Math.sqrt ( Adw.lerp (min_radius * min_radius, max_radius * max_radius, t)); } } private void invalidate_max_value () { this.max_value = double.NAN; this.queue_allocate (); } private void ensure_max_value () { if (this.max_value.is_nan ()) { this.max_value = this.calculate_max_value (); } } private void ensure_categories (uint count) { var previous_count = this.categories.length; if (previous_count >= count) { return; } this.categories.resize ((int) count); for (var index = previous_count; index < count; index++) { this.categories[index] = Category () { label = "", color = this.get_color () }; } // this.queue_update (); } private bool grow_buckets (uint rows, uint columns) { var resized = false; if (this.buckets == null) { this.buckets = new Bucket[rows, columns]; resized = true; } else { rows = uint.max (this.buckets.length[0], rows); columns = uint.max (this.buckets.length[1], columns); } if (this.buckets.length[0] != rows || this.buckets.length[1] != columns) { var rows_intersection = int.min ((int) rows, this.buckets.length[0]); var columns_intersection = int.min ((int) columns, this.buckets.length[1]); var buckets = new Bucket[rows, columns]; for (var row = 0; row < rows_intersection; row++) { for (var column = 0; column < columns_intersection; column++) { buckets[row, column] = this.buckets[row, column]; } } this.buckets = buckets; resized = true; } return resized; } private bool grow_data (uint row_count, uint column_count, uint category_count) { if (this.data == null) { row_count = uint.max (row_count, 1U); column_count = uint.max (column_count, 1U); category_count = uint.max (category_count, 1U); this.data = new Ft.Matrix3D (row_count, column_count, category_count); this.queue_update (); } else { row_count = uint.max (this.data.shape[0], row_count); column_count = uint.max (this.data.shape[1], column_count); category_count = uint.max (this.data.shape[2], category_count); } if (this.data.shape[0] != row_count || this.data.shape[1] != column_count || this.data.shape[2] != category_count) { this.data.resize (row_count, column_count, category_count, DEFAULT_VALUE); this.queue_update (); return true; } else { return false; } } private void queue_draw_bubbles () { unowned var bubble = this.bubbles_grid.get_first_child (); while (bubble != null) { bubble.queue_draw (); bubble = bubble.get_next_sibling (); } } private void queue_resize_bubbles () { unowned var bubble = this.bubbles_grid.get_first_child (); while (bubble != null) { bubble.queue_resize (); bubble = bubble.get_next_sibling (); } } private void ensure_bubble (uint row, uint column) { unowned var existing_bubble = this.bubbles_grid.get_child_at ((int) column, (int) row); if (existing_bubble == null) { var bubble = this.create_bubble (row, column); this.bubbles_grid.attach (bubble, (int) column, (int) row); } } public void set_category_label (uint category_index, string label) { this.ensure_categories (category_index + 1); this.categories[category_index].label = label; } public void set_category_color (uint category_index, Gdk.RGBA color) { this.ensure_categories (category_index + 1); this.categories[category_index].color = color; this.queue_draw (); } public void set_category_unit (uint category_index, Ft.Unit unit) { this.ensure_categories (category_index + 1); this.categories[category_index].unit = unit; } public double get_category_total (uint category_index) { return this.data.get_matrix (-1, (int) category_index).sum (); } private string get_tooltip_label (uint row, uint column) { if (row >= this.buckets.length[0] || column >= this.buckets.length[1]) { return ""; } return this.buckets[row, column].tooltip_label; } public void set_bubble_tooltip_label (uint row, uint column, string label) { if (this.grow_buckets (row + 1, column + 1)) { this.queue_update (); } this.buckets[row, column].tooltip_label = label; } public void set_bubble_inverted (uint row, uint column, bool inverted) { this.ensure_bubble (row, column); unowned var bubble = this.bubbles_grid.get_child_at ((int) column, (int) row); if (inverted) { bubble.add_css_class ("inverted"); } else { bubble.remove_css_class ("inverted"); } this.queue_update (); } private void update_bubbles () { var row_count = uint.max (this.data.shape[0], this.buckets.length[0]); var column_count = uint.max (this.data.shape[1], this.buckets.length[1]); if (this.data.shape[0] > this.buckets.length[0] || this.data.shape[1] > this.buckets.length[1]) { GLib.warning ("Missing bucket definitions: %dx%d vs %ux%u", this.buckets.length[1], this.buckets.length[0], this.data.shape[1], this.data.shape[0]); } for (var row = 0U; row < row_count; row++) { for (var column = 0U; column < column_count; column++) { this.ensure_bubble (row, column); } } } private void update_rows_labels () { var count = this.rows_labels.length; var box = this.rows_box; var child = box.get_first_child (); for (var index = 0; index < count; index++) { if (child == null) { var label = new Gtk.Label (this.rows_labels[index]); label.xalign = 1.0f; label.yalign = 0.5f; box.append (label); child = (Gtk.Widget) label; } else { ((Gtk.Label) child).label = this.rows_labels[index]; } child = child.get_next_sibling (); } while (child != null) { var next_child = child.get_next_sibling (); box.remove (child); child = next_child; } } private void update_columns_labels () { var count = this.columns_labels.length; var box = this.columns_box; var child = box.get_first_child (); for (var index = 0; index < count; index++) { if (child == null) { var label = new Gtk.Label (this.columns_labels[index]); label.xalign = 0.5f; label.yalign = 0.5f; box.append (label); child = (Gtk.Widget) label; } else { ((Gtk.Label) child).label = this.columns_labels[index]; } child = child.get_next_sibling (); } while (child != null) { var next_child = child.get_next_sibling (); box.remove (child); child = next_child; } } private void update () { if (this.update_idle_id != 0) { this.remove_tick_callback (this.update_idle_id); this.update_idle_id = 0; } this.update_bubbles (); this.update_rows_labels (); this.update_columns_labels (); } private void queue_update () { if (this.update_idle_id != 0) { return; } this.update_idle_id = this.add_tick_callback (() => { this.update_idle_id = 0; this.update (); return GLib.Source.REMOVE; }); } private inline void ensure_rows (uint count) { if (this.rows_labels.length < count) { this.rows_labels.resize ((int) count); } } private inline void ensure_columns (uint count) { if (this.columns_labels.length < count) { this.columns_labels.resize ((int) count); } } public void set_row_label (uint row, string label) { this.ensure_rows (uint.max ( row + 1U, this.data != null ? this.data.shape[0] : 0U)); this.rows_labels[row] = label; this.queue_update (); } public void set_column_label (uint column, string label) { this.ensure_columns (uint.max ( column + 1U, this.data != null ? this.data.shape[1] : 0U)); this.columns_labels[column] = label; this.queue_update (); } public void fill (double value) { this.grow_data (uint.max (this.rows_labels.length, this.buckets.length[0]), uint.max (this.columns_labels.length, this.buckets.length[1]), this.categories.length); this.data.fill (value); this.invalidate_max_value (); this.queue_update (); } public void set_values (uint row, uint column, double[] values) { this.grow_data (row + 1, column + 1, values.length); var category_index = 0; for (; category_index < values.length; category_index++) { this.data.@set ((int) row, (int) column, category_index, values[category_index]); } for (; category_index < this.data.shape[2]; category_index++) { this.data.@set ((int) row, (int) column, category_index, 0.0); } this.invalidate_max_value (); } public double get_value (uint row, uint column, uint category_index) { return this.data != null ? this.data.@get ((int) row, (int) column, (int) category_index, DEFAULT_VALUE) : DEFAULT_VALUE; } public void set_value (uint row, uint column, uint category_index, double value) { this.grow_data (row + 1, column + 1, category_index + 1); this.data.@set ((int) row, (int) column, (int) category_index, value); this.invalidate_max_value (); } public inline void add_value (uint row, uint column, uint category_index, double value) { this.set_value (row, column, category_index, value + this.get_value (row, column, category_index)); } private Gtk.Widget? create_tooltip_widget (uint row, uint column) { var category_count = this.categories.length; var grid = new Gtk.Grid (); grid.column_spacing = 10; grid.row_spacing = 5; grid.row_homogeneous = true; grid.add_css_class ("tooltip-contents"); var header_label = new Gtk.Label (this.get_tooltip_label (row, column)); header_label.add_css_class ("tooltip-header"); grid.attach (header_label, 0, 0, 2, 1); for (var category_index = category_count - 1; category_index >= 0; category_index--) { var category = this.categories[category_index]; var category_value = this.get_value (row, column, category_index); var category_label = new Gtk.Label (@"$(category.label):"); category_label.halign = Gtk.Align.START; grid.attach (category_label, 0, 1 + category_index); var value_label = new Gtk.Label ( this.format_tooltip_value (category_index, category_value)); value_label.halign = Gtk.Align.END; grid.attach (value_label, 1, 1 + category_index); } return grid; } private void measure_bubble (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = MIN_BUBBLE_SIZE; natural = this.bubble_size; minimum_baseline = -1; natural_baseline = -1; } private void snapshot_bubble (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var row = gizmo.get_data ("row"); var column = gizmo.get_data ("column"); var category_index = (uint) this.category; var bubble_value = this.get_value (row, column, category_index); var bubble_radius = this.calculate_bubble_radius (bubble_value); var bubble_origin = Graphene.Point () { x = ((float) gizmo.get_width ()) / 2.0f, y = ((float) gizmo.get_height ()) / 2.0f }; var bubble_inverted = gizmo.has_css_class ("inverted"); var bubble_color = this.categories[category_index].color; var is_empty = this.level_radii.length > 0 ? bubble_radius == this.level_radii[0] : false; if (is_empty && bubble_inverted) { return; } if (is_empty || bubble_inverted) { bubble_color.alpha = 0.1f; } var path_builder = new Gsk.PathBuilder (); path_builder.add_circle (bubble_origin, bubble_radius); if (bubble_inverted) { bubble_color.alpha *= 0.9f; var stroke = new Gsk.Stroke (2.2f); snapshot.append_stroke (path_builder.to_path (), stroke, bubble_color); } else { snapshot.append_fill (path_builder.to_path (), Gsk.FillRule.WINDING, bubble_color); } } private bool contains_bubble (Ft.Gizmo gizmo, double x, double y) { if (!gizmo.get_mapped ()) { return false; } // Check if cursor fits within bubble boundaries. var point = Graphene.Point () { x = (float) x, y = (float) y }; Graphene.Rect bounds; if (!gizmo.compute_bounds (gizmo, out bounds)) { return false; } return bounds.contains_point (point); } private Gtk.Widget? create_bubble (uint row, uint column) { var bubble = new Ft.Gizmo (BubbleChart.measure_bubble_cb, null, BubbleChart.snapshot_bubble_cb, BubbleChart.contains_bubble_cb, null, null); bubble.focusable = false; bubble.has_tooltip = true; bubble.add_css_class ("bubble"); bubble.set_data ("row", row); bubble.set_data ("column", column); bubble.query_tooltip.connect (BubbleChart.on_query_tooltip_cb); if (this.activate_on_click) { unowned var weak_bubble = bubble; var click_gesture = new Gtk.GestureClick (); click_gesture.set_button (Gdk.BUTTON_PRIMARY); click_gesture.released.connect ((n_press, x, y) => { BubbleChart.on_clicked_cb (weak_bubble); }); bubble.add_controller (click_gesture); } else { bubble.set_state_flags (Gtk.StateFlags.INSENSITIVE, false); } return (Gtk.Widget) bubble; } private static Ft.BubbleChart? from_gizmo (Ft.Gizmo gizmo) { Gtk.Widget? widget = gizmo; while (widget != null) { var chart = widget as Ft.BubbleChart; if (chart != null) { return chart; } widget = widget.get_parent (); } return null; } private static Ft.BubbleChart? from_widget (Gtk.Widget widget) { Gtk.Widget? current = widget; while (current != null) { var chart = current as Ft.BubbleChart; if (chart != null) { return chart; } current = current.get_parent (); } return null; } private static void measure_bubble_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var self = BubbleChart.from_gizmo (gizmo); if (self != null) { self.measure_bubble (gizmo, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } } private static void snapshot_bubble_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = BubbleChart.from_gizmo (gizmo); if (self != null) { self.snapshot_bubble (gizmo, snapshot); } } private static bool contains_bubble_cb (Ft.Gizmo gizmo, double x, double y) { var self = BubbleChart.from_gizmo (gizmo); return self != null ? self.contains_bubble (gizmo, x, y) : false; } private static bool on_query_tooltip_cb (Gtk.Widget widget, int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { var self = BubbleChart.from_widget (widget); var row = widget.get_data ("row"); var column = widget.get_data ("column"); if (self == null) { return false; } if (self.tooltip_row != row || self.tooltip_column != column) { self.tooltip_row = (int) row; self.tooltip_column = (int) column; self.tooltip_widget = self.create_tooltip_widget (row, column); } tooltip.set_custom (self.tooltip_widget); return self.tooltip_widget != null; } private static void on_clicked_cb (Gtk.Widget widget) { var self = BubbleChart.from_gizmo ((Ft.Gizmo) widget); var row = widget.get_data ("row"); var column = widget.get_data ("column"); if (self == null) { return; } var bubble_value = self.get_value (row, column, (uint) self.category); if (!bubble_value.is_nan () && bubble_value > 0.0) { self.bubble_activated (row, column); } } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var columns_box_minimum = 0; var columns_box_natural = 0; var rows_box_minimum = 0; var rows_box_natural = 0; var rows = int.max ( this.data != null ? (int) this.data.shape[0] : 0, this.rows_labels.length); var columns = int.max ( this.data != null ? (int) this.data.shape[1] : 0, this.columns_labels.length); this.columns_box.measure ( orientation, for_size, out columns_box_minimum, out columns_box_natural, null, null); this.rows_box.measure ( orientation, for_size, out rows_box_minimum, out rows_box_natural, null, null); if (orientation == Gtk.Orientation.HORIZONTAL) { minimum = rows_box_minimum + this.layout_grid.column_spacing + int.max ( MIN_BUBBLE_SIZE * columns + this.bubbles_grid.column_spacing * (columns - 1), columns_box_minimum); natural = rows_box_natural + this.layout_grid.column_spacing + int.max ( MIN_BUBBLE_SIZE * columns + this.bubbles_grid.column_spacing * (columns - 1), columns_box_natural); } else { var minimum_bubble_size = MIN_BUBBLE_SIZE; var natural_bubble_size = MIN_BUBBLE_SIZE; if (for_size > 0 && columns > 0) { var total_column_spacing = this.layout_grid.column_spacing + this.bubbles_grid.column_spacing * (columns - 1); var rows_box_minimum_width = 0; var rows_box_natural_width = 0; this.rows_box.measure (Gtk.Orientation.HORIZONTAL, -1, out rows_box_minimum_width, out rows_box_natural_width, null, null); minimum_bubble_size = ( (for_size - rows_box_minimum_width - total_column_spacing) / columns ).clamp (MIN_BUBBLE_SIZE, MAX_BUBBLE_SIZE); natural_bubble_size = ( (for_size - rows_box_natural_width - total_column_spacing) / columns ).clamp (MIN_BUBBLE_SIZE, MAX_BUBBLE_SIZE); } minimum = columns_box_minimum + this.layout_grid.row_spacing + int.max ( minimum_bubble_size * rows + this.bubbles_grid.row_spacing * (rows - 1), rows_box_minimum); natural = columns_box_natural + this.layout_grid.row_spacing + int.max ( natural_bubble_size * rows + this.bubbles_grid.row_spacing * (rows - 1), rows_box_natural); } if (natural < minimum) { natural = minimum; } minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { var rows = int.max ( this.data != null ? (int) this.data.shape[0] : 0, this.rows_labels.length); var columns = int.max ( this.data != null ? (int) this.data.shape[1] : 0, this.columns_labels.length); var columns_box_height = 0; var rows_box_width = 0; this.ensure_max_value (); this.columns_box.measure ( Gtk.Orientation.VERTICAL, -1, null, out columns_box_height, null, null); this.rows_box.measure ( Gtk.Orientation.HORIZONTAL, height - columns_box_height - this.layout_grid.row_spacing, null, out rows_box_width, null, null); var total_column_spacing = this.layout_grid.column_spacing + this.bubbles_grid.column_spacing * (columns - 1); var total_row_spacing = this.layout_grid.row_spacing + this.bubbles_grid.row_spacing * (rows - 1); var h_bubble_size = columns > 0 ? (width - rows_box_width - total_column_spacing) / columns : MIN_BUBBLE_SIZE; var v_bubble_size = rows > 0 ? (height - columns_box_height - total_row_spacing) / rows : MIN_BUBBLE_SIZE; var bubble_size = int.min (h_bubble_size, v_bubble_size).clamp ( MIN_BUBBLE_SIZE, MAX_BUBBLE_SIZE); if (this.bubble_size != bubble_size) { this.bubble_size = bubble_size; this.update_level_radii (); this.queue_resize_bubbles (); } var allocation = Gtk.Allocation () { width = width, height = height }; this.layout_grid.allocate_size (allocation, -1); } public override void snapshot (Gtk.Snapshot snapshot) { this.snapshot_child (this.layout_grid, snapshot); } public override void dispose () { this.data = null; this.categories = null; this.buckets = null; this.rows_labels = null; this.columns_labels = null; this.level_radii = null; this.tooltip_widget = null; base.dispose (); } public signal void bubble_activated (uint row, uint column); } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/canvas-layout.vala000066400000000000000000000222311520625676500270640ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * A `Gtk.LayoutChild` for `Ft.Canvas` widgets, storing per-child layout properties */ public sealed class CanvasLayoutChild : Gtk.LayoutChild { /** * Absolute position in the value space. */ public float x { get; set; default = 0.0f; } /** * Absolute position in the value space. */ public float y { get; set; default = 0.0f; } /** * Offset from the widget's top-left corner in pixels. */ public int x_origin { get; set; default = 0; } /** * Offset from the widget's top-left corner in pixels. */ public int y_origin { get; set; default = 0; } internal Graphene.Rect range; internal Gdk.Rectangle absolute_bounds; /** * Calculate widget's bounds. The bounds position is in relative units from its origin. */ public Gdk.Rectangle calculate_bounds () { var bounds = Gdk.Rectangle () { x = -this.x_origin, y = -this.y_origin, width = 0, height = 0 }; var widget = this.get_child_widget (); if (widget != null) { widget.measure (Gtk.Orientation.HORIZONTAL, -1, null, out bounds.width, null, null); widget.measure (Gtk.Orientation.VERTICAL, -1, null, out bounds.height, null, null); } return bounds; } public void set_range (float x_from, float x_to, float y_from, float y_to) { this.range = Graphene.Rect (); this.range.init (x_from, y_from, x_to - x_from, y_to - y_from); } } public sealed class CanvasLayout : Gtk.LayoutManager { private const double EPSILON = 0.00001; /** * Calculate child's bounds in `Canvas` coordinates. */ private inline Gdk.Rectangle calculate_child_bounds (Ft.CanvasLayoutChild child, float x_scale, float y_scale) { child.absolute_bounds = child.calculate_bounds (); // cache result child.absolute_bounds.x += (int) Math.roundf (x_scale * child.x); child.absolute_bounds.y -= (int) Math.roundf (y_scale * child.y); return child.absolute_bounds; } /** * Calculate bounds for all children in `Canvas` coordinates. */ private Gdk.Rectangle calculate_bounds (Gtk.Widget widget) { var canvas = (Ft.Canvas) widget; var x_scale = canvas.x_scale; var y_scale = canvas.y_scale; var bounds = Gdk.Rectangle (); var is_first_child = true; for (var child_widget = widget.get_first_child (); child_widget != null; child_widget = child_widget.get_next_sibling ()) { var layout_child = (Ft.CanvasLayoutChild) this.get_layout_child (child_widget); if (layout_child == null) { continue; } var child_bounds = this.calculate_child_bounds (layout_child, x_scale, y_scale); if (is_first_child) { bounds = child_bounds; is_first_child = false; } else { bounds.union (child_bounds, out bounds); } } return bounds; } private inline int calculate_width (Gtk.Widget widget) { // XXX: use cached bounds if possible return int.max (this.calculate_bounds (widget).width, widget.width_request); } private inline int calculate_height (Gtk.Widget widget) { // XXX: use cached bounds if possible return int.max (this.calculate_bounds (widget).height, widget.height_request); } /** * Calculate value bounds aka value range */ public Graphene.Rect calculate_range (Gtk.Widget widget) { var range = Graphene.Rect (); var is_first_child = true; for (var child_widget = widget.get_first_child (); child_widget != null; child_widget = child_widget.get_next_sibling ()) { var layout_child = (Ft.CanvasLayoutChild) this.get_layout_child (child_widget); if (layout_child == null) { continue; } if (is_first_child) { range = layout_child.range; is_first_child = false; } else { range = range.union (layout_child.range); } } return range; } /** * Calculate and canvas origin point * * The origin point is absolute (centre of coordinate system). */ public bool calculate_origin (Gtk.Widget widget, out int x_origin, out int y_origin) { var canvas = widget as Ft.Canvas; if (canvas != null) { // XXX: cache bounds if possible var bounds = this.calculate_bounds (widget); var x_offset = int.max (widget.width_request - bounds.width, 0); var y_offset = int.max (widget.height_request - bounds.height, 0); x_origin = -bounds.x + x_offset; y_origin = -bounds.y + y_offset; return true; } else { x_origin = 0; y_origin = 0; return false; } } /* * LayoutManger */ public override Gtk.LayoutChild create_layout_child (Gtk.Widget widget, Gtk.Widget for_child) { return (Gtk.LayoutChild) GLib.Object.@new (typeof (Ft.CanvasLayoutChild), "layout-manager", this, "child-widget", for_child); } public override Gtk.SizeRequestMode get_request_mode (Gtk.Widget widget) { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Widget widget, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { natural = orientation == Gtk.Orientation.HORIZONTAL ? this.calculate_width (widget) : this.calculate_height (widget); minimum = natural; minimum_baseline = -1; natural_baseline = -1; } /** * Place items for given size. * * We likely execute it having `origin` point computed earlier. */ public override void allocate (Gtk.Widget widget, int width, int height, int baseline) { var canvas = (Ft.Canvas) widget; canvas.update_origin (); var x_origin = canvas.x_origin; var y_origin = canvas.y_origin; // Translate widgets to top-left corner and allocate for (var child_widget = widget.get_first_child (); child_widget != null; child_widget = child_widget.get_next_sibling ()) { var layout_child = (Ft.CanvasLayoutChild?) this.get_layout_child (child_widget); if (layout_child == null) { continue; } var child_allocation = (Gtk.Allocation) layout_child.absolute_bounds; child_allocation.x += x_origin; child_allocation.y += y_origin; child_widget.allocate_size (child_allocation, -1); } } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/canvas.vala000066400000000000000000000246171520625676500255630ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * A container for placing widgets at given coordinates in a user-defined value space. * Value space is defined by `x-scale` and `y-scale`. `Canvas` will stretch to accommodate all * children. Then it's expected that parent widget will position the canvas, using `transform` * for alignment. * * There is no sub-pixel accuracy here. Items positions are rounded to nearest pixels. * * Unlike in GTK+, the y-coordinates increase when going up (like in typical charts). * Items are positioned from bottom-left corner. * Worth a read: https://docs.gtk.org/gtk4/coordinates.html */ public sealed class Canvas : Gtk.Widget { private const double EPSILON = 0.00001; public float x_scale { get { return this._x_scale; } set { if (this._x_scale == value) { return; } this._x_scale = value; this.invalidate_transform (); this.queue_resize (); this.queue_resize_children (); } } public float y_scale { get { return this._y_scale; } set { if (this._y_scale == value) { return; } this._y_scale = value; this.invalidate_transform (); this.queue_resize (); this.queue_resize_children (); } } /** * Offset between top-left corner and an origin point. */ [CCode (notify = false)] public int x_origin { get { return this._x_origin; } } /** * Offset between top-left corner and an origin point. */ [CCode (notify = false)] public int y_origin { get { return this._y_origin; } } private float _x_scale = 1.0f; private float _y_scale = 1.0f; private int _x_origin = 0; private int _y_origin = 0; private Gsk.Transform? transform = null; private Gsk.Transform? transform_inv = null; static construct { set_css_name ("canvas"); set_layout_manager_type (typeof (Ft.CanvasLayout)); } private void invalidate_transform () { this.transform = null; this.transform_inv = null; } private void queue_resize_children () { for (var child = this.get_first_child (); child != null; child = child.get_next_sibling ()) { child.queue_resize (); } } /** * Set-up the value space. We only need `x-scale` and `y-scale`. */ public void set_scale (float x_scale, float y_scale) requires (x_scale.is_finite ()) requires (y_scale.is_finite ()) { var x_scale_changed = x_scale != this._x_scale; var y_scale_changed = y_scale != this._y_scale; this._x_scale = x_scale; this._y_scale = y_scale; if (x_scale_changed) { this.notify_property ("x-scale"); } if (y_scale_changed) { this.notify_property ("y-scale"); } if (x_scale_changed || y_scale_changed) { this.invalidate_transform (); this.queue_resize (); this.queue_resize_children (); } } private void set_origin (int x_origin, int y_origin) { var x_origin_changed = x_origin != this._x_origin; var y_origin_changed = y_origin != this._y_origin; this._x_origin = x_origin; this._y_origin = y_origin; if (x_origin_changed) { this.notify_property ("x-origin"); } if (y_origin_changed) { this.notify_property ("y-origin"); } if (x_origin_changed || y_origin_changed) { this.invalidate_transform (); } } internal void update_origin () { var layout = (Ft.CanvasLayout) this.layout_manager; int x_origin, y_origin; if (layout.calculate_origin (this, out x_origin, out y_origin)) { this.set_origin (x_origin, y_origin); } } private void update_transform () { if (this._x_scale.abs () > EPSILON && this._y_scale.abs () > EPSILON) { var transform = new Gsk.Transform (); transform = transform.scale (1.0f / this._x_scale, -1.0f / this._y_scale); transform = transform.translate ( Graphene.Point () { x = (float)(-this._x_origin), y = (float)(-this._y_origin) }); this.transform = transform; this.transform_inv = transform.invert (); } else { this.transform = null; this.transform_inv = null; } } /** * Transform point from widget coordinates to value coordinates */ public Graphene.Point transform_point (Graphene.Point point) { if (this.transform == null) { this.update_transform (); } return this.transform != null ? this.transform.transform_point (point) : point; } /** * Transform point from value coordinates to widget coordinates */ public Graphene.Point transform_point_inv (Graphene.Point point) { if (this.transform_inv == null) { this.update_transform (); } return this.transform_inv != null ? this.transform_inv.transform_point (point) : point; } public void calculate_range (out float x_from, out float x_to, out float y_from, out float y_to) { var layout = (Ft.CanvasLayout) this.layout_manager; var range = layout.calculate_range (this); x_from = range.origin.x; x_to = range.origin.x + range.size.width; y_from = range.origin.y; y_to = range.origin.y + range.size.height; } /* * Children */ /** * Get the layout child for a widget (for advanced usage) */ internal inline unowned Ft.CanvasLayoutChild? get_layout_child (Gtk.Widget child) { return (Ft.CanvasLayoutChild?) this.layout_manager?.get_layout_child (child); } /** * Add a widget to the canvas at the given position */ public void add_child (Gtk.Widget child, float x = 0.0f, float y = 0.0f) { child.set_parent (this); var layout_child = this.get_layout_child (child); layout_child.x = x; layout_child.y = y; this.queue_resize (); } /** * Remove a widget from the canvas */ public void remove_child (Gtk.Widget child) { child.unparent (); this.queue_resize (); } /** * Set the position of a widget in the canvas */ public void set_child_position (Gtk.Widget child, float x, float y) { var layout_child = this.get_layout_child (child); if (layout_child != null) { layout_child.x = x; layout_child.y = y; } this.queue_resize (); } /** * Get the position of a widget in the canvas */ public void get_child_position (Gtk.Widget child, out float x, out float y) { var layout_child = this.get_layout_child (child); if (layout_child != null) { x = layout_child.x; y = layout_child.y; } else { x = 0.0f; y = 0.0f; } // this.queue_resize (); } /** * Set the origin offset of a widget */ public void set_child_origin (Gtk.Widget child, int x_origin, int y_origin) { var layout_child = this.get_layout_child (child); if (layout_child != null) { layout_child.x_origin = x_origin; layout_child.y_origin = y_origin; } // this.queue_resize (); } /** * Set value range of a widget */ public void set_child_range (Gtk.Widget child, float x_from, float x_to, float y_from, float y_to) { var layout_child = this.get_layout_child (child); if (layout_child != null) { layout_child.range = Graphene.Rect (); layout_child.range.init (x_from, y_from, x_to - x_from, y_to - y_from); } // this.queue_resize (); } public override void dispose () { Gtk.Widget? child; while ((child = this.get_first_child ()) != null) { child.unparent (); } this.transform = null; this.transform_inv = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/chart-axis.vala000066400000000000000000000535361520625676500263550ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [CCode (scope = "weak")] public delegate string FormatValueFunc (double value); public class ChartAxis : Gtk.Widget { private const double EPSILON = 0.00001; private const uint MAX_TICKS = 24; // How much we allow to go beyond value range to display a tick private const float EXTENT = 0.2f; public Gtk.Orientation orientation { get { return this._orientation; } construct { this._orientation = value; } } public float value_from { get { return this._value_from; } } public float value_to { get { return this._value_to; } } public float value_spacing { get { return this._value_spacing; } } public float scale { get { return this._scale; } set { if (this._scale != value) { this._scale = value; } } } public int text_offset { get { return this._text_offset; } set { if (this._text_offset != value) { this._text_offset = value; } } } public bool continous { get { return this._continous; } set { this._continous = value; this.queue_resize (); } } public int origin { get { return this._origin; } set { this._origin = value; this.queue_resize (); } } internal int label_width; internal int label_height; private Gtk.Orientation _orientation = Gtk.Orientation.HORIZONTAL; private float _value_from = 0.0f; private float _value_to = 1.0f; private float _value_spacing = 1.0f; private float _scale = 1.0f; private int _text_offset = 0; private bool _continous = true; private int _origin = 0; private float[] ticks = null; private Pango.Layout[] layouts = null; private Ft.FormatValueFunc? format_value_func = null; static construct { set_css_name ("chartaxis"); } construct { this.layouts = {}; } public ChartAxis (Gtk.Orientation orientation, owned Ft.FormatValueFunc? format_value_func = null) { GLib.Object ( orientation: orientation, focusable: false ); this.format_value_func = (owned) format_value_func; } private static float[] create_ticks (float value_from, float value_to, float value_spacing) requires (value_spacing.abs () > EPSILON) { var tick_from = (int) Math.floorf (value_from / value_spacing); var tick_to = int.max ((int) Math.ceilf (value_to / value_spacing), tick_from + 1); var tick_count = tick_to > tick_from ? tick_to - tick_from + 1 : 0; var tick_values = new float[tick_count]; for (var index = 0; index < tick_values.length; index++) { tick_values[index] = (float)(tick_from + index) * value_spacing; } value_from = tick_values[0]; value_to = tick_values[tick_values.length - 1]; return tick_values; } private inline Pango.Alignment get_text_alignment () { switch (this._orientation) { case Gtk.Orientation.HORIZONTAL: return Pango.Alignment.CENTER; case Gtk.Orientation.VERTICAL: return Pango.Alignment.RIGHT; default: assert_not_reached (); } } private inline string format_value (float value) { return this.format_value_func != null ? this.format_value_func ((double) value) : value.to_string (); } /** * Determine optimal ticks to display on an axis */ private void calculate_ticks (float value_from, float value_to, float value_spacing, int available_size, out float[] ticks, out int label_width, out int label_height) { // Define possible tick values float[] tick_values; if (this._continous) { var value_from_extended = value_from - (value_to - value_from) * EXTENT; var value_to_extended = value_to + (value_to - value_from) * EXTENT; if (value_from >= 0.0f && value_from_extended < 0.0f) { value_from_extended = value_from; } if (value_to <= 0.0f && value_to_extended > 0.0f) { value_to_extended = value_to; } tick_values = create_ticks (value_from_extended, value_to_extended, value_spacing); } else { tick_values = create_ticks (value_from, value_to, value_spacing); } // Find index of the origin point var origin_value = 0.0f; var origin_index = (int) Math.roundf ((origin_value - tick_values[0]) / value_spacing); // Prepare a Pango layout to estimate label size var context = this.create_pango_context (); var layout = new Pango.Layout (context); layout.set_width (-1); var label_sizes = new int[tick_values.length, 2]; for (var tick_index = 0; tick_index < tick_values.length; tick_index++) { label_sizes[tick_index, 0] = -1; label_sizes[tick_index, 1] = -1; } // Determine optimal stride and offset var min_stride = int.max (tick_values.length / (int) MAX_TICKS, 1); var max_stride = int.max (tick_values.length / 2, 1); var best_stride = 0; var best_offset = 0; var best_tick_count = 0; var best_label_width = 0; var best_label_height = 0; var best_score = 0.0f; for (var candidate_stride = max_stride; candidate_stride >= min_stride; candidate_stride--) { // Calculate optimal offset and tick_count var tick_index_from = origin_index - candidate_stride * (int) Math.roundf ( (value_from - origin_value) / (value_spacing * (float) candidate_stride)); var tick_index_to = origin_index + candidate_stride * (int) Math.roundf ( (value_to - origin_value) / (value_spacing * (float) candidate_stride)); if (tick_index_from < 0) { tick_index_from = 0; } if (tick_index_to > tick_values.length - 1) { tick_index_to = tick_values.length - 1; } if (tick_index_to == tick_index_from) { if (tick_index_to + candidate_stride >= tick_values.length - 1) { tick_index_to += candidate_stride; } else if (tick_index_from - candidate_stride >= 0) { tick_index_from -= candidate_stride; } } var candidate_offset = tick_index_from; var candidate_tick_count = (tick_index_to - tick_index_from) / candidate_stride + 1; if (candidate_tick_count < 2) { continue; } // Estimate label size for the candidate stride and offset var candidate_label_width = 0; var candidate_label_height = 0; for (var tick_index = tick_index_from; tick_index <= tick_index_to; tick_index += candidate_stride) { if (label_sizes[tick_index, 0] < 0 || label_sizes[tick_index, 1] < 0) { var tick_label = this.format_value (tick_values[tick_index]); layout.set_text (tick_label, tick_label.length); layout.get_pixel_size (out label_sizes[tick_index, 0], out label_sizes[tick_index, 1]); } candidate_label_width = int.max (label_sizes[tick_index, 0], candidate_label_width); candidate_label_height = int.max (label_sizes[tick_index, 1], candidate_label_height); } // Check if the number of ticks can fit the `available_size` var candidate_label_size = this._orientation == Gtk.Orientation.HORIZONTAL ? candidate_label_width : candidate_label_height; var candidate_spacing = this._orientation == Gtk.Orientation.HORIZONTAL ? candidate_label_size : candidate_label_size * 2; var candidate_size = (candidate_label_size + candidate_spacing) * (candidate_tick_count - 1) + candidate_label_size; if (candidate_size > available_size) { if (best_stride == 0) { continue; } else { // Reducing stride further is unlikely to get better candidates break; } } // Select a candidate that spans the most and has the most ticks. var candidate_score = (float) candidate_tick_count * (float) candidate_size; if (candidate_score >= best_score) { best_stride = candidate_stride; best_offset = candidate_offset; best_tick_count = candidate_tick_count; best_label_width = candidate_label_width; best_label_height = candidate_label_height; best_score = candidate_score; } } if (best_tick_count >= 2) { ticks = new float[best_tick_count]; label_width = best_label_width; label_height = best_label_height; for (var tick_index = 0; tick_index < ticks.length; tick_index++) { ticks[tick_index] += tick_values[tick_index * best_stride + best_offset]; } } else { // Not a likely scenario GLib.warning ("Could not determine axis ticks for range %f to %f and spacing %f", value_from, value_to, value_spacing); ticks = new float[2]; label_width = 0; label_height = 0; ticks[0] = Math.roundf ((value_from - origin_value) / value_spacing) * value_spacing + origin_value; ticks[1] = ticks[0] + value_spacing; for (var tick_index = 0; tick_index < ticks.length; tick_index++) { var tick_label = this.format_value (ticks[tick_index]); var tmp_label_width = 0; var tmp_label_height = 0; layout.set_text (tick_label, tick_label.length); layout.get_pixel_size (out tmp_label_width, out tmp_label_height); label_width = int.max (label_width, tmp_label_width); label_height = int.max (label_height, tmp_label_height); } } } public void set_format_value_func (owned Ft.FormatValueFunc? func) { this.format_value_func = (owned) func; this.queue_resize (); } /** * Determine displayed value range calculate ticks. */ public void configure (float value_from, float value_to, float value_spacing, int available_size) { this.calculate_ticks (value_from, value_to, value_spacing, available_size, out this.ticks, out this.label_width, out this.label_height); this._value_from = float.min (this.ticks[0], value_from); this._value_to = float.max (this.ticks[this.ticks.length - 1], value_to); this.configured (); this.queue_resize (); } public float[] get_ticks () { return this.ticks; } public Ft.ChartAxis detach () { var clone = (Ft.ChartAxis) GLib.Object.@new ( typeof (Ft.ChartAxis), orientation: this._orientation); var initial_ref_count = this.ref_count; weak Ft.ChartAxis self = this; GLib.WeakRef weak_clone = GLib.WeakRef (clone); ulong configured_id = 0, destroy_id = 0, clone_destroy_id = 0; clone.set_format_value_func ( (value) => { return self.format_value_func != null ? self.format_value_func (value) : value.to_string (); }); this.bind_property ("scale", clone, "scale", GLib.BindingFlags.SYNC_CREATE); this.bind_property ("text-offset", clone, "text-offset", GLib.BindingFlags.SYNC_CREATE); this.bind_property ("origin", clone, "origin", GLib.BindingFlags.SYNC_CREATE); configured_id = this.configured.connect ( () => { var _clone = (Ft.ChartAxis) weak_clone.get (); if (_clone != null) { _clone._value_to = self._value_to; _clone.label_width = self.label_width; _clone.label_height = self.label_height; _clone.ticks = self.ticks; } }); destroy_id = this.destroy.connect ( () => { var _clone = (Ft.ChartAxis) weak_clone.get (); if (_clone != null) { _clone.set_format_value_func (null); } if (_clone != null && clone_destroy_id != 0) { _clone.disconnect (clone_destroy_id); clone_destroy_id = 0; } if (configured_id != 0) { self.disconnect (configured_id); configured_id = 0; } if (destroy_id != 0) { self.disconnect (destroy_id); destroy_id = 0; } }); clone_destroy_id = clone.destroy.connect ( () => { var _clone = (Ft.ChartAxis) weak_clone.get (); if (_clone != null) { _clone.set_format_value_func (null); } if (_clone != null && clone_destroy_id != 0) { _clone.disconnect (clone_destroy_id); clone_destroy_id = 0; } if (configured_id != 0) { self.disconnect (configured_id); configured_id = 0; } if (destroy_id != 0) { self.disconnect (destroy_id); destroy_id = 0; } }); this.visible = false; assert (this.ref_count == initial_ref_count); return clone; } public inline bool is_detached () { return !this.visible; } /* * Widget */ public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var label_size = orientation == Gtk.Orientation.HORIZONTAL ? this.label_width : this.label_height; natural = this._orientation == orientation ? label_size / 2 + this.origin : label_size + this.text_offset; minimum = natural; minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { var context = this.create_pango_context (); var text_alignment = this.get_text_alignment (); this.layouts = new Pango.Layout[this.ticks.length]; for (var tick_index = 0; tick_index < this.ticks.length; tick_index++) { var tick_value = this.ticks[tick_index]; var tick_label = this.format_value (tick_value); var layout = new Pango.Layout (context); layout.set_width (this.label_width * Pango.SCALE); layout.set_alignment (text_alignment); layout.set_ellipsize (Pango.EllipsizeMode.NONE); layout.set_wrap (Pango.WrapMode.WORD); layout.set_text (tick_label, tick_label.length); this.layouts[tick_index] = layout; } } public override void snapshot (Gtk.Snapshot snapshot) { var tick_count = int.min (this.ticks.length, this.layouts.length); if (this.ticks.length != this.layouts.length) { GLib.warning ("Number of layouts (%d) does not match the number of ticks (%d)", this.layouts.length, this.ticks.length); } var color = this.get_color (); var scale = this._orientation == Gtk.Orientation.HORIZONTAL ? this._scale : -this._scale; for (var tick_index = 0; tick_index < tick_count; tick_index++) { var tick_value = this.ticks[tick_index]; unowned var layout = this.layouts[tick_index]; var layout_position = (float) this._origin + tick_value * scale; var layout_offset = (float) this._text_offset; var layout_width = (float) this.label_width; var layout_height = (float) this.label_height; Graphene.Point layout_origin; switch (this._orientation) { case Gtk.Orientation.HORIZONTAL: layout_origin = Graphene.Point () { x = layout_position - layout_width / 2.0f, y = layout_offset }; break; case Gtk.Orientation.VERTICAL: layout_origin = Graphene.Point () { x = (float) this.get_width () - layout_offset - layout_width, y = layout_position - layout_height / 2.0f }; break; default: assert_not_reached (); } // `append_layout` places the layout with bottom-left corner as its origin snapshot.save (); snapshot.translate (layout_origin); snapshot.append_layout (layout, color); snapshot.restore (); } } public signal void configured (); public override void dispose () { this.ticks = null; this.layouts = null; this.format_value_func = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/chart-contents.vala000066400000000000000000000304051520625676500272340ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { /** * Extra wrapper around canvas for displaying axes and grid over charts content. */ // TODO: implement viewport/scrollable interface public sealed class ChartContents : Gtk.Widget { private const int AXIS_TEXT_OFFSET = 8; public Ft.Canvas canvas { get { return this._canvas; } } public Ft.ChartAxis x_axis { get { return this._x_axis; } } public Ft.ChartAxis y_axis { get { return this._y_axis; } } public Ft.ChartGrid grid { get { return this._grid; } } public int x_origin { get { return this._x_origin; } } public int y_origin { get { return this._y_origin; } } public float y_value_from { get { return this._y_value_from; } set { if (this._y_value_from == value) { return; } this._y_value_from = value; this.queue_resize (); } } public float y_value_to { get { return this._y_value_to; } set { if (this._y_value_to == value) { return; } this._y_value_to = value; this.queue_resize (); } } private Ft.Canvas? _canvas = null; private Ft.ChartAxis? _x_axis = null; private Ft.ChartAxis? _y_axis = null; private Ft.ChartGrid? _grid = null; private int _x_origin = 0; private int _y_origin = 0; private float _y_value_from = float.NAN; private float _y_value_to = float.NAN; private weak Ft.Chart? chart = null; private bool is_empty = true; construct { this.accessible_role = Gtk.AccessibleRole.IMG; this.add_css_class ("contents"); this._canvas = new Ft.Canvas (); this._x_axis = new Ft.ChartAxis (Gtk.Orientation.HORIZONTAL); this._x_axis.text_offset = AXIS_TEXT_OFFSET; this._y_axis = new Ft.ChartAxis (Gtk.Orientation.VERTICAL); this._y_axis.text_offset = AXIS_TEXT_OFFSET; this._grid = new Ft.ChartGrid (this.x_axis, this._y_axis); this._grid.insert_before (this, null); this._x_axis.insert_before (this, null); this._y_axis.insert_before (this, null); this._canvas.insert_before (this, null); } private unowned Ft.Chart? get_chart () { if (this.chart != null) { return this.chart; } var widget = this.parent; while (widget != null) { if (widget is Ft.Chart) { this.chart = (Ft.Chart) widget; return this.chart; } widget = widget.parent; } return null; } /** * Key function for calculating contents layout. * * Updates children if needed. It's the place where we compute scale. */ internal void configure_axes (int chart_width, int chart_height) { var chart = this.get_chart (); if (chart == null) { return; } chart.update_canvas (this._canvas); // Check value ranges and prepare axes ticks float x_value_from, x_value_to, y_value_from, y_value_to; this._canvas.calculate_range (out x_value_from, out x_value_to, out y_value_from, out y_value_to); if (this._y_value_from.is_finite ()) { y_value_from = float.min (y_value_from, this._y_value_from); } if (this._y_value_to.is_finite ()) { y_value_to = float.max (y_value_to, this._y_value_to); } this._x_axis.configure (x_value_from, x_value_to, chart.x_spacing, chart_width); this._y_axis.configure (y_value_from, y_value_to, chart.y_spacing, chart_height); this.queue_resize (); } public override Gtk.SizeRequestMode get_request_mode () { var chart = this.get_chart (); return chart != null ? chart.get_contents_request_mode (this._canvas) : Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var chart = this.get_chart (); if (chart != null) { chart.measure_canvas (this._canvas, orientation, for_size, out minimum, out natural); if (minimum > natural) { minimum = natural; } if (this._y_axis.visible) { if (orientation == Gtk.Orientation.HORIZONTAL) { // XXX: it's not precise - we ignore x-axis `label_width` minimum += this._y_axis.text_offset; natural += this._y_axis.text_offset; } } if (this._x_axis.visible) { if (orientation == Gtk.Orientation.VERTICAL) { minimum += this._x_axis.label_height + this._x_axis.text_offset; natural += this._x_axis.label_height + this._x_axis.text_offset; } } } else { minimum = 0; natural = 0; } minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { // By now axes should be configured. var canvas_width = width; var canvas_height = height; canvas_height -= this._x_axis.visible ? this._x_axis.label_height + this._x_axis.text_offset : 0; canvas_width -= this._y_axis.visible ? this._y_axis.label_width + this._y_axis.text_offset : this._y_axis.text_offset; // Measure working area Gdk.Rectangle canvas_working_area; chart.measure_working_area (this._canvas, canvas_width, canvas_height, out canvas_working_area); normalize_rectangle (ref canvas_working_area); this.is_empty = canvas_working_area.width == 0 || canvas_working_area.height == 0; if (this.is_empty) { return; } // Calculate scale var x_scale = (float) ( (double) canvas_working_area.width / (double) (this._x_axis.value_to - this._x_axis.value_from)); var y_scale = (float) ( (double) canvas_working_area.height / (double) (this._y_axis.value_to - this._y_axis.value_from)); this._canvas.set_scale (x_scale, y_scale); this._x_axis.scale = x_scale; this._y_axis.scale = y_scale; // Calculate layout // If an axis is not visible its position will be negative var canvas_x_offset = this._x_axis.label_width / 2 - canvas_working_area.x; var y_axis_allocation = Gtk.Allocation () { x = this._y_axis.visible ? 0 : -this._y_axis.label_width, y = 0, width = this._y_axis.label_width + this._y_axis.text_offset, height = height }; var x_axis_allocation = Gtk.Allocation () { x = y_axis_allocation.x + y_axis_allocation.width + int.max (0 - canvas_x_offset, 0), height = this._x_axis.label_height + this._x_axis.text_offset }; x_axis_allocation.y = height - (this._x_axis.visible ? x_axis_allocation.height : 0); x_axis_allocation.width = width - x_axis_allocation.x; var grid_allocation = Gtk.Allocation () { x = y_axis_allocation.x + y_axis_allocation.width - this._y_axis.text_offset, y = 0, height = height - x_axis_allocation.y }; grid_allocation.width = width - grid_allocation.x; var canvas_allocation = Gtk.Allocation () { x = y_axis_allocation.x + y_axis_allocation.width + int.max (canvas_x_offset, 0), y = 0, width = canvas_width, height = canvas_height }; // Sync origin this._canvas.update_origin (); this._x_origin = canvas_allocation.x + this._canvas.x_origin; this._y_origin = canvas_allocation.y + this._canvas.y_origin; this._x_axis.origin = this._x_origin - x_axis_allocation.x; this._y_axis.origin = this._y_origin - y_axis_allocation.y; this._grid.x_origin = this._x_origin - grid_allocation.x; this._grid.y_origin = this._y_origin - grid_allocation.y; // Allocate children this._canvas.allocate_size (canvas_allocation, -1); if (this._x_axis.visible) { this._x_axis.allocate_size (x_axis_allocation, -1); } if (this._y_axis.visible) { this._y_axis.allocate_size (y_axis_allocation, -1); } if (this._grid.visible) { this._grid.allocate_size (grid_allocation, -1); } } public override void snapshot (Gtk.Snapshot snapshot) { if (this.is_empty) { return; } for (var child = this.get_first_child (); child != null; child = child.get_next_sibling ()) { if (child.visible) { this.snapshot_child (child, snapshot); } } } public override void unrealize () { base.unrealize (); this.chart = null; } public override void dispose () { this.chart = null; if (this._grid != null) { this._grid.unparent (); this._grid = null; } if (this._x_axis != null) { this._x_axis.unparent (); this._x_axis = null; } if (this._y_axis != null) { this._y_axis.unparent (); this._y_axis = null; } if (this._canvas != null) { this._canvas.unparent (); this._canvas = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/chart-grid.vala000066400000000000000000000101641520625676500263240ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public class ChartGrid : Gtk.Widget { public Ft.ChartAxis x_axis { get { return this._x_axis; } } public Ft.ChartAxis y_axis { get { return this._y_axis; } } public int x_origin { get { return this._x_origin; } set { this._x_origin = value; } } public int y_origin { get { return this._y_origin; } set { this._y_origin = value; } } public float line_width { get { return this._line_width; } set { this._line_width = value; this.queue_draw (); } } public bool horizontal { get { return this._horizontal; } set { this._horizontal = value; this.queue_draw (); } } public bool vertical { get { return this._vertical; } set { this._vertical = value; this.queue_draw (); } } private unowned Ft.ChartAxis? _x_axis = null; private unowned Ft.ChartAxis? _y_axis = null; private int _x_origin = 0; private int _y_origin = 0; private float _line_width = 1.0f; private bool _horizontal = true; private bool _vertical = true; static construct { set_css_name ("chartgrid"); } public ChartGrid (Ft.ChartAxis? x_axis, Ft.ChartAxis? y_axis) { this._x_axis = x_axis; this._y_axis = y_axis; if (this._x_axis != null) { this._x_axis.configured.connect (() => this.queue_draw ()); } if (this._y_axis != null) { this._y_axis.configured.connect (() => this.queue_draw ()); } } public override void snapshot (Gtk.Snapshot snapshot) { var width = (float) this.get_width (); var height = (float) this.get_height (); var stroke = new Gsk.Stroke (this._line_width); var path_builder = new Gsk.PathBuilder (); // round line positions to full pixels var line_offset = ((this._line_width - 1.0f) % 2.0f - 1.0f).abs () * 0.5f; var color = this.get_color (); color.alpha *= 0.5f; if (this.horizontal && this._y_axis != null) { var y_scale = -this._y_axis.scale; foreach (var tick_value in this._y_axis.get_ticks ()) { var line_y = Math.roundf (tick_value * y_scale + (float) this._y_origin) + line_offset; path_builder.move_to (0.0f, line_y); path_builder.line_to (width, line_y); } } if (this.vertical && this._x_axis != null) { var x_scale = this._x_axis.scale; foreach (var tick_value in this._x_axis.get_ticks ()) { var line_x = Math.roundf (tick_value * x_scale + (float) this._x_origin) + line_offset; path_builder.move_to (line_x, 0.0f); path_builder.line_to (line_x, height); } } snapshot.append_stroke (path_builder.to_path (), stroke, color); } public override void dispose () { this._x_axis = null; this._y_axis = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/chart.ui000066400000000000000000000025521520625676500250750ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/charts/chart.vala000066400000000000000000000364431520625676500254110ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public enum Unit { AMOUNT, PERCENT, INTERVAL; public string format (double value) { if (value.is_nan ()) { return "–"; } switch (this) { case AMOUNT: var value_rounded = (long) Math.round (value * 10.0); var number = value_rounded / 10; var decimal = value_rounded.abs () % 10; return decimal == 0 ? number.to_string () : @"$(number).$(decimal)"; case PERCENT: var value_rounded = (int) Math.round (100.0 * value); return @"$(value_rounded)%"; case INTERVAL: // round +/- 10s var value_rounded = 20.0 * Math.round (value / 20.0); return Ft.Interval.format_short ( Ft.Interval.from_seconds (value_rounded)); default: assert_not_reached (); } } } private struct Category { public string label; public Gdk.RGBA color; public Ft.Unit unit; public bool visible; } private struct Bucket { public string label; public string tooltip_label; } /** * Base class for drawing a 2D charts with X and Y axes. * * The content is scrollable horizontally. */ [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/charts/chart.ui")] public abstract class Chart : Gtk.Widget, Gtk.Buildable { private const int MIN_WIDTH = 300; private const int MIN_HEIGHT = 150; private const double EPSILON = 0.00001; /** * Interval between ticks on x axis */ public float x_spacing { get { return this._x_spacing; } set { if (this._x_spacing == value) { return; } this._x_spacing = value; this.queue_allocate (); } } /** * Interval between ticks on y axis */ public float y_spacing { get { return this._y_spacing; } set { if (this._y_spacing == value) { return; } this._y_spacing = value; this.queue_allocate (); } } public float aspect_ratio { get { return this._aspect_ratio; } set { this._aspect_ratio = value; this.queue_resize (); } } [GtkChild] protected unowned Ft.ChartContents contents; [GtkChild] private unowned Gtk.ScrolledWindow scrolled_window; private Ft.ChartAxis? y_axis = null; private float _x_spacing = 1.0f; private float _y_spacing = 1.0f; private float _aspect_ratio = 1.0f; private double drag_start_x; private bool zooming = false; private double zoom_x_value; private double zoom_y_value; private double zoom_x; private double zoom_y; private FormatValueFunc? format_value_func = null; static construct { set_css_name ("chart"); } construct { unowned var self = this; this.contents.x_axis.set_format_value_func ( (value) => { return self.format_x_value (value); }); this.contents.y_axis.set_format_value_func ( (value) => { return self.format_y_value (value); }); this.y_axis = this.contents.y_axis.detach (); this.y_axis.insert_before (this, null); // HACK: counteract delegates/self increasing `this.ref_count` this.@unref (); } public void set_format_value_func (owned Ft.FormatValueFunc? func) { this.format_value_func = (owned) func; this.queue_allocate (); } public virtual string format_x_value (double value) { return this.format_value_func != null ? this.format_value_func (value) : "%.2f".printf (value); } public virtual string format_y_value (double value) { return this.format_value_func != null ? this.format_value_func (value) : "%.2f".printf (value); } [GtkCallback] private void on_drag_begin (double start_x, double start_y) { this.drag_start_x = this.scrolled_window.hadjustment.value; } [GtkCallback] private void on_pan (Gtk.GesturePan gesture, Gtk.PanDirection direction, double offset) { if (direction == Gtk.PanDirection.LEFT) { offset = -offset; } this.scrolled_window.hadjustment.value = this.drag_start_x - offset; } private bool get_pointer_position (out double x, out double y) { double px, py; double nx, ny; Graphene.Point point; var native = this.get_native (); var surface = native?.get_surface (); var pointer = surface?.get_display ().get_default_seat ()?.get_pointer (); if (pointer == null) { x = double.NAN; y = double.NAN; return false; } surface.get_device_position (pointer, out px, out py, null); native.get_surface_transform (out nx, out ny); var surface_point = Graphene.Point (); surface_point.init ((float)(px - nx), (float)(py - ny)); if (native.compute_point (this, surface_point, out point)) { x = (double) point.x; y = (double) point.y; return true; } else { x = double.NAN; y = double.NAN; return false; } } [GtkCallback] private bool on_scroll (Gtk.EventControllerScroll controller, double dx, double dy) { var event = controller.get_current_event (); double x, y; if (event == null) { return false; } if ((event.get_modifier_state () & Gdk.ModifierType.CONTROL_MASK) == 0) { return false; } if (!this.get_pointer_position (out x, out y)) { return false; } var point = Graphene.Point (); point.init ((float) x, (float) y); var contents_point = Graphene.Point (); if (!this.compute_point (this.contents, point, out contents_point)) { return false; } contents_point = this.contents.canvas.transform_point (contents_point); if (dy < 0.0) { this.zoom_begin (contents_point.x, contents_point.y, x, y); this.zoom_in (); } else if (dy > 0.0) { this.zoom_begin (contents_point.x, contents_point.y, x, y); this.zoom_out (); } return true; } public abstract Gtk.SizeRequestMode get_contents_request_mode (Ft.Canvas canvas); /** * Create and position canvas items in the value space. */ public abstract void update_canvas (Ft.Canvas canvas); public abstract void measure_canvas (Ft.Canvas canvas, Gtk.Orientation orientation, int for_size, out int minimum, out int natural); /** * A method for calculating items size before allocation. It's expected that you update * items origin point for the widgets. * * At this point canvas scale is not calculated yet, nor we know items final positions * at the pixel-level. * * If size exceeds `available_width`, the content will be scrolled horizontally. The * `working_area` represents area available for drawing values. */ public abstract void measure_working_area (Ft.Canvas canvas, int available_width, int available_height, out Gdk.Rectangle working_area); protected void queue_update () { this.queue_resize (); } protected virtual void zoom_begin (double x_value, double y_value, double x, double y) { this.zoom_x_value = x_value; this.zoom_y_value = y_value; this.zoom_x = x; this.zoom_y = y; this.zooming = true; } protected virtual void zoom_end (double x_value, double y_value, double x, double y) { // Convert point from value coordinates to chart coordinates var contents_point = Graphene.Point (); contents_point.init ((float) x_value, (float) y_value); contents_point = this.contents.canvas.transform_point_inv (contents_point); var point = Graphene.Point (); if (this.contents.compute_point (this, contents_point, out point)) { // Adjust scroll, so that anchor point is preserved var hadjustment = this.scrolled_window.hadjustment; hadjustment.value = (hadjustment.value + point.x - x).clamp ( hadjustment.lower, hadjustment.upper); // TODO: vadjustment } this.zooming = false; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { this.scrolled_window.measure (orientation, for_size, out minimum, out natural, null, null); if (orientation == Gtk.Orientation.HORIZONTAL) { minimum = int.max (MIN_WIDTH, minimum); natural = int.max (minimum, this.width_request); } else { minimum = int.max (MIN_HEIGHT, minimum); natural = int.max (minimum, this.height_request); // Grow the hight to met the requested aspect ratio if (for_size > 0 && this.aspect_ratio > 0.0f) { natural = int.max ( (int) Math.roundf ((float) for_size / this.aspect_ratio), minimum); } } minimum_baseline = -1; natural_baseline = -1; } /** * Calculate chart layout * * Base class allocates axes, handles content sizing. Content may be scrolled horizontally * if there is not enough space. */ public override void size_allocate (int width, int height, int baseline) { this.update_canvas (this.contents.canvas); this.contents.configure_axes (width, height); var y_axis_width = this.y_axis != null ? this.y_axis.label_width : 0; var scrolled_window_allocation = Gtk.Allocation () { x = y_axis_width, y = 0, width = width - y_axis_width, height = height }; this.scrolled_window.allocate_size (scrolled_window_allocation, -1); if (this.y_axis != null) { var y_axis_allocation = Gtk.Allocation () { x = 0, y = this.contents.y_origin - this.y_axis.origin }; this.y_axis.measure (Gtk.Orientation.HORIZONTAL, -1, null, out y_axis_allocation.width, null, null); this.y_axis.measure (Gtk.Orientation.VERTICAL, -1, null, out y_axis_allocation.height, null, null); this.y_axis.allocate_size (y_axis_allocation, -1); } if (this.zooming) { this.zoom_end (this.zoom_x_value, this.zoom_y_value, this.zoom_x, this.zoom_y); } } public override void snapshot (Gtk.Snapshot snapshot) { this.snapshot_child (this.scrolled_window, snapshot); if (this.y_axis != null) { this.snapshot_child (this.y_axis, snapshot); } } public signal void zoom_in (); public signal void zoom_out (); public override void dispose () { if (this.y_axis != null) { this.y_axis.unparent (); this.y_axis = null; } this.@ref (); this.contents.x_axis.set_format_value_func (null); this.contents.y_axis.set_format_value_func (null); this.scrolled_window.child = null; this.format_value_func = null; // HACK: Without this `GtkScrolledWindow` does not get disposed properly this.dispose_template (typeof (Ft.Chart)); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-day-page.ui000066400000000000000000000127451520625676500253400ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-day-page.vala000066400000000000000000000531731520625676500256460ustar00rootroot00000000000000/* * Copyright (c) 2017-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/stats-day-page.ui")] public class StatsDayPage : Adw.Bin, Ft.StatsPage { private const int64 DEFAULT_INTERVAL = 1 * Ft.Interval.HOUR; private const int64 MIN_INTERVAL = 15 * Ft.Interval.MINUTE; private const int64 MAX_INTERVAL = 2 * Ft.Interval.HOUR; // Default display hours for histogram x-axis private const int BASE_START_HOUR = 9; private const int BASE_END_HOUR = 17; // Minimum number of bars displayed private const int MIN_HISTOGRAM_BAR_COUNT = 8; // Minimum duration threshold for range expansion private const int64 MIN_SIGNIFICANT_DURATION = Ft.Interval.MINUTE; public GLib.Date date { get; construct; } [CCode (notify = false)] public int64 interval { get { return this._interval; } set { value = value.clamp (MIN_INTERVAL, MAX_INTERVAL); if (this._interval == value) { return; } this._interval = value; this.on_interval_notify (); this.notify_property ("interval"); } } [GtkChild] private unowned Gtk.Revealer toolbar_revealer; [GtkChild] private unowned Gtk.Button zoom_in_button; [GtkChild] private unowned Gtk.Button zoom_out_button; [GtkChild] private unowned Ft.BarChart histogram; [GtkChild] private unowned Ft.StatsCard pomodoro_card; [GtkChild] private unowned Ft.StatsCard breaks_card; [GtkChild] private unowned Ft.StatsCard interruptions_card; [GtkChild] private unowned Ft.StatsCard break_ratio_card; private int64 _interval = DEFAULT_INTERVAL; private Ft.StatsManager? stats_manager; private Ft.TimezoneHistory? timezone_history; private GLib.DateTime? datetime; private int64 timestamp; private int64 start_time; private int64 end_time; private int64 entries_start_time = Ft.Timestamp.UNDEFINED; private int64 entries_end_time = Ft.Timestamp.UNDEFINED; private string time_format; private Ft.Matrix? histogram_data; construct { this.stats_manager = new Ft.StatsManager (); this.timezone_history = new Ft.TimezoneHistory (); this.datetime = this.stats_manager.get_midnight (this.date); this.timestamp = Ft.Timestamp.from_datetime (this.datetime); this.histogram.set_category_label ( Ft.StatsCategory.POMODORO, _("Pomodoro")); this.histogram.set_category_unit ( Ft.StatsCategory.POMODORO, Ft.Unit.INTERVAL); this.histogram.set_category_label ( Ft.StatsCategory.BREAK, _("Breaks")); this.histogram.set_category_unit ( Ft.StatsCategory.BREAK, Ft.Unit.INTERVAL); this.update_time_format (); this.update_histogram_y_spacing (); this.stats_manager.entry_saved.connect (this.on_entry_saved); this.stats_manager.entry_deleted.connect (this.on_entry_deleted); this.populate.begin ( (obj, res) => { this.populate.end (res); }); } public StatsDayPage (GLib.Date date) { GLib.Object ( date: date ); } /** * Create data container sampled at `MIN_INTERVAL` for the whole day. * `this.histogram` by comparison is holding displayed time range only. */ private Ft.Matrix create_histogram_data () { // Add a buffer of 12h for potential timezone changes or other edge cases var bucket_count = (uint)(36 * Ft.Interval.HOUR / MIN_INTERVAL); return new Ft.Matrix (bucket_count, this.histogram.get_categories_count ()); } private static string format_hours (double value) { return Ft.Interval.format_short (Ft.Interval.from_seconds (value), Ft.Interval.HOUR); } private static string format_minutes (double value) { return Ft.Interval.format_short (Ft.Interval.from_seconds (value), Ft.Interval.MINUTE); } private inline string format_time (GLib.DateTime datetime) { var time_string = datetime.format (this.time_format); if (time_string.has_prefix ("0")) { time_string = time_string.substring (1); } return time_string; } private void ensure_histogram_data () { if (this.histogram_data == null) { this.histogram_data = this.create_histogram_data (); } } private void update_category_colors () { var foreground_color = this.histogram.get_color (); var pomodoro_color = Ft.get_chart_primary_color (foreground_color); var break_color = Ft.get_chart_secondary_color (foreground_color); this.histogram.set_category_color (Ft.StatsCategory.POMODORO, pomodoro_color); this.histogram.set_category_color (Ft.StatsCategory.BREAK, break_color); } private void update_time_format () { var use_12h_format = Ft.Locale.use_12h_format (); var time_format = "%H"; if (this._interval < Ft.Interval.HOUR || !use_12h_format) { time_format += ":%M"; } if (use_12h_format) { time_format = time_format.replace ("%H", "%I") + " %p"; } this.time_format = time_format; } private void update_histogram_y_spacing () { var y_spacing = this._interval <= Ft.Interval.HOUR ? (float) Ft.Interval.to_seconds (this._interval) / 3.0f : 3600.0f; this.histogram.y_spacing = y_spacing; this.histogram.reference_value = (float) Ft.Interval.to_seconds (this._interval); if (y_spacing >= 3600.0f) { this.histogram.set_format_value_func (format_hours); } else { this.histogram.set_format_value_func (format_minutes); } } private void update_histogram_buckets () { var datetimes = new GLib.DateTime[0]; var timestamp = this.start_time; this.timezone_history.scan ( this.start_time, this.end_time + this._interval, (start_time, end_time, timezone) => { while (timestamp < end_time) { datetimes += Ft.Timestamp.to_datetime (timestamp, timezone); timestamp += this._interval; } }); // Update bar labels this.histogram.remove_all_bars (); for (var bar_index = 0; bar_index < datetimes.length - 1; bar_index++) { var bar_label = this.format_time (datetimes[bar_index]); var tooltip_label = "%s - %s".printf ( bar_label, this.format_time (datetimes[bar_index + 1])); this.histogram.set_bar_label (bar_index, bar_label, tooltip_label); } } /** * Set a transform from `bar_index` to `bucket_index` */ private void update_histogram_transform () { var bars_per_bucket = this._interval / MIN_INTERVAL; var bucket_start_index = (this.start_time - this.timestamp) / MIN_INTERVAL; this.histogram.set_transform ( (double) bars_per_bucket, (double) bucket_start_index); } private async Gom.ResourceGroup? fetch_entries () { var repository = Ft.Database.get_repository (); var date_value = GLib.Value (typeof (string)); date_value.set_string (Ft.Database.serialize_date (this.date)); var date_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "date", date_value); var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.StatsEntry), "time", Gom.SortingMode.ASCENDING); try { var entries = yield repository.find_sorted_async (typeof (Ft.StatsEntry), date_filter, sorting); yield entries.fetch_async (0U, entries.count); return entries; } catch (GLib.Error error) { GLib.critical ("Error while fetching daily stats: %s", error.message); return null; } } private void extend_time_range (int64 entry_start_time, int64 entry_end_time) { var changed = false; if (this.entries_start_time > entry_start_time) { this.entries_start_time = entry_start_time; changed = true; } if (this.entries_end_time < entry_end_time) { this.entries_end_time = entry_end_time; changed = true; } if (changed) { this.update_time_range (); this.update_histogram_transform (); this.update_histogram_buckets (); } } private void process_entry (Ft.StatsEntry entry, int sign = 1) requires (sign == 1 || sign == -1) { GLib.return_if_fail (this.histogram_data != null); var entry_category = Ft.StatsCategory.from_string (entry.category); var entry_time = entry.time; var entry_duration = entry.duration; // Validate if entry is relevant if (entry_category == Ft.StatsCategory.INVALID) { return; } if (entry.date != Ft.Database.serialize_date (this.date)) { return; } // Validate time range if (entry_time < this.timestamp) { entry_duration -= this.timestamp - entry_time; entry_time = this.timestamp; } if (entry_category != Ft.StatsCategory.INTERRUPTION && entry_duration >= MIN_SIGNIFICANT_DURATION) { this.extend_time_range (entry_time, entry_time + entry_duration); } // Update histogram // Quantize entry range into buckets var bucket_index = (int) ((entry_time - this.timestamp) / MIN_INTERVAL); var category_index = (int) entry_category; var bars_per_bucket = (int) (this._interval / MIN_INTERVAL); var bucket_start_index = (int) ((this.start_time - this.timestamp) / MIN_INTERVAL); var remaining_duration = entry_duration; var remaining_offset = entry_time - (this.timestamp + bucket_index * MIN_INTERVAL); while (remaining_duration > 0 && bucket_index < this.histogram_data.shape[0] && category_index < this.histogram_data.shape[1] && category_index != Ft.StatsCategory.INTERRUPTION) { var consumed_duration = int64.min (remaining_duration, MIN_INTERVAL - remaining_offset); var bucket_value = Ft.Interval.to_seconds (sign * consumed_duration); this.histogram_data.add_value (bucket_index, category_index, bucket_value); if (bucket_index >= bucket_start_index) { var bar_index = (bucket_index - bucket_start_index) / bars_per_bucket; this.histogram.add_value (bar_index, category_index, bucket_value); } remaining_duration -= consumed_duration; remaining_offset = 0; bucket_index++; } // Update cards switch (entry_category) { case Ft.StatsCategory.POMODORO: this.pomodoro_card.value += Ft.Interval.to_seconds (sign * entry_duration); break; case Ft.StatsCategory.BREAK: this.breaks_card.value += Ft.Interval.to_seconds (sign * entry_duration); break; case Ft.StatsCategory.INTERRUPTION: this.interruptions_card.value += (double) sign; break; default: // no matching card in UI break; } var total = this.pomodoro_card.value + this.breaks_card.value; this.break_ratio_card.value = total >= 3600.0 ? this.breaks_card.value / total : double.NAN; } /** * Fill histogram with aggregated data */ private void aggregate_data () { this.histogram.fill (0.0); if (this.histogram_data == null) { return; } assert (this.start_time >= this.timestamp); var bucket_start_index = (int) ((this.start_time - this.timestamp) / MIN_INTERVAL); var bucket_end_index = (int) ((this.end_time - this.timestamp) / MIN_INTERVAL); var buckets_per_bar = (int) (this._interval / MIN_INTERVAL); var categories_count = (int) this.histogram_data.shape[1]; if (bucket_start_index < 0) { bucket_start_index = 0; } for (var category_index = 0; category_index < categories_count; category_index++) { for (var bucket_index = bucket_start_index; bucket_index < bucket_end_index; bucket_index++) { var bar_index = (bucket_index - bucket_start_index) / buckets_per_bar; var bucket_value = this.histogram_data.@get (bucket_index, category_index, 0.0); this.histogram.add_value (bar_index, category_index, bucket_value); } } } /** * Calculate time range for given entries */ private void update_entries_time_range (Gom.ResourceGroup? entries) { this.entries_start_time = Ft.Timestamp.UNDEFINED; this.entries_end_time = Ft.Timestamp.UNDEFINED; if (entries == null) { return; } for (var index = 0U; index < entries.count; index++) { var entry = (Ft.StatsEntry) entries.get_index (index); var entry_start_time = entry.time; var entry_end_time = entry.time + entry.duration; if (entry.duration < MIN_SIGNIFICANT_DURATION) { continue; } // XXX: we do not validate category here if (entry_start_time < this.entries_start_time || Ft.Timestamp.is_undefined (this.entries_start_time)) { this.entries_start_time = entry_start_time; } if (entry_end_time > this.entries_end_time || Ft.Timestamp.is_undefined (this.entries_end_time)) { this.entries_end_time = entry_end_time; } } } /** * Calculate display time range based on default work hours and entries */ private void update_time_range () { var midnight_hour = (int)(Ft.StatsManager.MIDNIGHT_OFFSET / Ft.Interval.HOUR); // Ensure time range includes working hours var start_time = Ft.Timestamp.from_datetime ( this.datetime.add_hours (BASE_START_HOUR - midnight_hour)); var end_time = Ft.Timestamp.from_datetime ( this.datetime.add_hours (BASE_END_HOUR - midnight_hour)); // Extend time range to entries if (Ft.Timestamp.is_defined (this.entries_start_time) && start_time > this.entries_start_time) { start_time = this.entries_start_time; } if (Ft.Timestamp.is_defined (this.entries_end_time) && end_time < this.entries_end_time) { end_time = this.entries_end_time; } // Round time range to `this._interval` start_time = this.timestamp + this._interval * ( (start_time - this.timestamp) / this._interval); end_time = (end_time - start_time) % this._interval != 0 ? start_time + this._interval * ((end_time - start_time) / this._interval + 1) : start_time + this._interval * ((end_time - start_time) / this._interval); // Ensure minimum number of bars when zoomed out var bar_count = (int)((end_time - start_time) / this._interval); if (bar_count < MIN_HISTOGRAM_BAR_COUNT) { start_time = int64.max ( this.timestamp, start_time - ((MIN_HISTOGRAM_BAR_COUNT - bar_count) / 2) * this._interval); end_time = int64.max ( end_time, start_time + MIN_HISTOGRAM_BAR_COUNT * this._interval); } this.start_time = start_time; this.end_time = end_time; } private void reset () { this.ensure_histogram_data (); this.histogram_data.fill (0.0); this.histogram.fill (0.0); this.pomodoro_card.value = 0.0; this.breaks_card.value = 0.0; this.interruptions_card.value = 0.0; this.break_ratio_card.value = double.NAN; } private async void populate () { var entries = yield this.fetch_entries (); this.update_entries_time_range (entries); this.update_time_range (); this.update_histogram_y_spacing (); this.update_histogram_transform (); this.update_histogram_buckets (); this.reset (); for (var index = 0U; index < entries.count; index++) { this.process_entry ((Ft.StatsEntry) entries.get_index (index)); } } private void invalidate_histogram_data () { this.histogram_data = null; } private void on_interval_notify () { this.update_time_format (); this.update_time_range (); this.update_histogram_y_spacing (); this.update_histogram_transform (); this.update_histogram_buckets (); this.aggregate_data (); this.zoom_in_button.sensitive = this._interval > MIN_INTERVAL; this.zoom_out_button.sensitive = this._interval < MAX_INTERVAL; } private void on_entry_saved (Ft.StatsEntry entry) { if (this.histogram_data != null) { this.process_entry (entry, 1); } } private void on_entry_deleted (Ft.StatsEntry entry) { if (this.histogram_data != null) { this.process_entry (entry, -1); } } private void on_timezone_history_changed () { if (this.get_mapped ()) { this.populate.begin ( (obj, res) => { this.populate.end (res); }); } else { this.invalidate_histogram_data (); } } [GtkCallback] private void on_zoom_in () { this.interval = (this._interval / 2).clamp (MIN_INTERVAL, MAX_INTERVAL); } [GtkCallback] private void on_zoom_out () { this.interval = (this._interval * 2).clamp (MIN_INTERVAL, MAX_INTERVAL); } [GtkCallback] private void on_histogram_enter (double x, double y) { this.toolbar_revealer.reveal_child = true; } [GtkCallback] private void on_histogram_leave () { this.toolbar_revealer.reveal_child = false; } public override void css_changed (Gtk.CssStyleChange change) { base.css_changed (change); this.update_category_colors (); } public override void dispose () { this.histogram.set_format_value_func (null); this.stats_manager.entry_saved.disconnect (this.on_entry_saved); this.stats_manager.entry_deleted.disconnect (this.on_entry_deleted); this.timezone_history.changed.disconnect (this.on_timezone_history_changed); this.stats_manager = null; this.timezone_history = null; this.datetime = null; this.histogram_data = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-month-page.ui000066400000000000000000000052661520625676500257100ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-month-page.vala000066400000000000000000000330011520625676500262020ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/stats-month-page.ui")] public class StatsMonthPage : Adw.Bin, Ft.StatsPage { private const int DAYS_PER_WEEK = 7; public GLib.Date date { get; construct; } [GtkChild] private unowned Ft.BubbleChart bubble_chart; [GtkChild] private unowned Ft.StatsCard pomodoro_card; [GtkChild] private unowned Ft.StatsCard breaks_card; [GtkChild] private unowned Ft.StatsCard interruptions_card; [GtkChild] private unowned Ft.StatsCard break_ratio_card; private Ft.StatsManager? stats_manager; private GLib.Date display_start_date; private GLib.Date display_end_date; construct { this.stats_manager = new Ft.StatsManager (); this.stats_manager.entry_saved.connect (this.on_entry_saved); this.stats_manager.entry_deleted.connect (this.on_entry_deleted); this.bubble_chart.category = Ft.StatsCategory.POMODORO; this.bubble_chart.set_category_label ( Ft.StatsCategory.POMODORO, _("Pomodoro")); this.bubble_chart.set_category_unit ( Ft.StatsCategory.POMODORO, Ft.Unit.INTERVAL); this.bubble_chart.set_category_label ( Ft.StatsCategory.BREAK, _("Breaks")); this.bubble_chart.set_category_unit ( Ft.StatsCategory.BREAK, Ft.Unit.INTERVAL); this.bubble_chart.set_category_label ( Ft.StatsCategory.INTERRUPTION, _("Interruptions")); this.bubble_chart.set_category_unit ( Ft.StatsCategory.INTERRUPTION, Ft.Unit.AMOUNT); var month = this.date.get_month (); var year = this.date.get_year (); var month_start_date = GLib.Date (); month_start_date.set_dmy (1, month, year); var month_end_date = GLib.Date (); month_end_date.set_dmy (GLib.Date.get_days_in_month (month, year), month, year); this.display_start_date = Ft.Timeframe.WEEK.normalize_date (month_start_date); this.display_end_date = Ft.Timeframe.WEEK.normalize_date (month_end_date); this.display_end_date.add_days (6U); this.populate.begin ( (obj, res) => { this.populate.end (res); }); } public StatsMonthPage (GLib.Date date) { GLib.Object ( date: date ); } private bool transform_date (GLib.Date date, out uint row, out uint column) { var position = this.display_start_date.days_between (date); if (position >= 0) { column = position % DAYS_PER_WEEK; row = position / DAYS_PER_WEEK; return true; } else { column = 0; row = 0; return false; } } private GLib.Date transform_position (uint row, uint column) { var date = this.display_start_date.copy (); date.add_days (DAYS_PER_WEEK * row + column); return date; } private void reset () { this.bubble_chart.fill (0.0); this.pomodoro_card.value = 0.0; this.breaks_card.value = 0.0; this.interruptions_card.value = 0.0; this.break_ratio_card.value = double.NAN; } private void update_category_colors () { var foreground_color = this.bubble_chart.get_color (); var pomodoro_color = Ft.get_chart_primary_color (foreground_color); var break_color = Ft.get_chart_secondary_color (foreground_color); this.bubble_chart.set_category_color (Ft.StatsCategory.POMODORO, pomodoro_color); this.bubble_chart.set_category_color (Ft.StatsCategory.BREAK, break_color); } private void update_bubble_chart_labels () { var year_start_date = GLib.Date (); year_start_date.set_dmy (1, 1, this.date.get_year ()); var date = this.display_start_date.copy (); var first_day_of_week = Ft.Locale.get_first_day_of_week (); var week_number_offset = 1U - ( first_day_of_week == GLib.DateWeekday.MONDAY ? year_start_date.get_monday_week_of_year () : year_start_date.get_sunday_week_of_year () ); var row = 0; var column = 0; while (date.compare (this.display_end_date) <= 0) { var tooltip_label = capitalize_words ( Ft.DateUtils.format_date (date, "%e %B")); this.bubble_chart.set_bubble_tooltip_label (row, column, tooltip_label); if (row == 0) { var weekday_name = capitalize_words ( Ft.DateUtils.format_date (date, "%a")); this.bubble_chart.set_column_label (column, weekday_name); } if (column == 0) { var week_end_date = date.copy (); week_end_date.add_days (6U); var week_number = ( first_day_of_week == GLib.DateWeekday.MONDAY ? week_end_date.get_monday_week_of_year () : week_end_date.get_sunday_week_of_year () ) + week_number_offset; this.bubble_chart.set_row_label (row, @"$(week_number)"); } if (date.get_month () != this.date.get_month ()) { this.bubble_chart.set_bubble_inverted (row, column, true); } date.add_days (1U); column++; if (column >= DAYS_PER_WEEK) { column = 0; row++; } } } private async Gom.ResourceGroup? fetch_aggregated_entries () { var repository = Ft.Database.get_repository (); var start_date_value = GLib.Value (typeof (string)); start_date_value.set_string (Ft.Database.serialize_date (this.display_start_date)); var end_date_value = GLib.Value (typeof (string)); end_date_value.set_string (Ft.Database.serialize_date (this.display_end_date)); var start_date_filter = new Gom.Filter.gte ( typeof (Ft.AggregatedStatsEntry), "date", start_date_value); var end_date_filter = new Gom.Filter.lte ( typeof (Ft.AggregatedStatsEntry), "date", end_date_value); var date_filter = new Gom.Filter.and (start_date_filter, end_date_filter); try { var aggregated_entries = yield repository.find_async ( typeof (Ft.AggregatedStatsEntry), date_filter); yield aggregated_entries.fetch_async (0U, aggregated_entries.count); return aggregated_entries; } catch (GLib.Error error) { GLib.critical ("Error while fetching weekly stats: %s", error.message); return null; } } private void process_aggregated_entry (Ft.AggregatedStatsEntry entry) { var entry_category = Ft.StatsCategory.from_string (entry.category); var entry_date = Ft.Database.parse_date (entry.date); var entry_duration = entry.duration; // Validate if entry is relevant if (entry_category == Ft.StatsCategory.INVALID) { return; } // Update bubble chart var category_index = (uint) entry_category; uint row, column; if (!this.transform_date (entry_date, out row, out column)) { return; } var bucket_value = entry_category != Ft.StatsCategory.INTERRUPTION ? Ft.Interval.to_seconds (entry_duration) : (double) entry.count; this.bubble_chart.add_value (row, column, category_index, bucket_value); // Update cards if (entry_date.get_month () == this.date.get_month ()) { switch (entry_category) { case Ft.StatsCategory.POMODORO: this.pomodoro_card.value += bucket_value; break; case Ft.StatsCategory.BREAK: this.breaks_card.value += bucket_value; break; case Ft.StatsCategory.INTERRUPTION: this.interruptions_card.value += bucket_value; break; default: // no matching card in UI break; } var total = this.pomodoro_card.value + this.breaks_card.value; this.break_ratio_card.value = total >= 3600.0 ? this.breaks_card.value / total : double.NAN; } } private void process_entry (Ft.StatsEntry entry, int sign = 1) requires (sign == 1 || sign == -1) { var entry_category = Ft.StatsCategory.from_string (entry.category); var entry_date = Ft.Database.parse_date (entry.date); var entry_duration = entry.duration; // Validate if entry is relevant if (entry_category == Ft.StatsCategory.INVALID) { return; } // Validate date range if (entry_date.compare (this.display_start_date) < 0 || entry_date.compare (this.display_end_date) > 0) { return; } // Update bubble chart uint row, column; if (!this.transform_date (entry_date, out row, out column)) { return; } var bucket_value = entry_category != Ft.StatsCategory.INTERRUPTION ? Ft.Interval.to_seconds (sign * entry_duration) : (double) sign; this.bubble_chart.add_value (row, column, (uint) entry_category, bucket_value); // Update cards if (entry_date.get_month () != this.date.get_month ()) { return; } switch (entry_category) { case Ft.StatsCategory.POMODORO: this.pomodoro_card.value += bucket_value; break; case Ft.StatsCategory.BREAK: this.breaks_card.value += bucket_value; break; case Ft.StatsCategory.INTERRUPTION: this.interruptions_card.value += bucket_value; break; default: // no matching card in UI break; } var total = this.pomodoro_card.value + this.breaks_card.value; this.break_ratio_card.value = total >= 3600.0 ? this.breaks_card.value / total : double.NAN; } private async void populate () { var aggregated_entries = yield this.fetch_aggregated_entries (); this.update_bubble_chart_labels (); this.reset (); for (var index = 0U; index < aggregated_entries.count; index++) { this.process_aggregated_entry ( (Ft.AggregatedStatsEntry) aggregated_entries.get_index (index)); } } private void on_entry_saved (Ft.StatsEntry entry) { this.process_entry (entry, 1); } private void on_entry_deleted (Ft.StatsEntry entry) { this.process_entry (entry, -1); } [GtkCallback] private void on_bubble_activated (uint row, uint column) { var date = this.transform_position (row, column); this.activate_action_variant ( "stats.select-day", Ft.DateUtils.date_to_variant (date)); } public override void css_changed (Gtk.CssStyleChange change) { base.css_changed (change); this.update_category_colors (); } public override void dispose () { if (this.stats_manager != null) { this.stats_manager.entry_saved.disconnect (this.on_entry_saved); this.stats_manager.entry_deleted.disconnect (this.on_entry_deleted); this.stats_manager = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-page.vala000066400000000000000000000014361520625676500250660ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public enum StatsCategory { POMODORO = 0, BREAK = 1, INTERRUPTION = 2, INVALID = -1; public static int from_string (string category) { switch (category) { case "pomodoro": return POMODORO; case "break": return BREAK; case "interruption": return INTERRUPTION; default: return INVALID; } } } public interface StatsPage : Gtk.Widget { public abstract GLib.Date date { get; construct; } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-view.ui000066400000000000000000000267031520625676500246220ustar00rootroot00000000000000
Day stats.set-timeframe day Week stats.set-timeframe week Month stats.set-timeframe month
focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-view.vala000066400000000000000000001226451520625676500251320ustar00rootroot00000000000000/* * Copyright (c) 2017, 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public enum Timeframe { DAY, WEEK, MONTH; public static Ft.Timeframe from_string (string? timeframe) { switch (timeframe) { case "day": return DAY; case "week": return WEEK; case "month": return MONTH; default: return DAY; } } public string to_string () { switch (this) { case DAY: return "day"; case WEEK: return "week"; case MONTH: return "month"; default: assert_not_reached (); } } public string get_label () { switch (this) { case DAY: return _("Day"); case WEEK: return _("Week"); case MONTH: return _("Month"); default: assert_not_reached (); } } public GLib.Date normalize_date (GLib.Date date) { var normalized_date = date.copy (); if (!normalized_date.valid ()) { return normalized_date; } switch (this) { case DAY: break; case WEEK: var first_day_of_week = Ft.Locale.get_first_day_of_week (); var offset = (int) date.get_weekday () - (int) first_day_of_week; if (offset < 0) { offset += 7; } normalized_date.subtract_days (offset); break; case MONTH: normalized_date.set_day (1); break; default: assert_not_reached (); } return normalized_date; } } public enum NavigationDirection { FORWARD, BACKWARD } public class StatsViewModel : GLib.Object { [CCode (notify = false)] public Ft.Timeframe timeframe { get { return this._timeframe; } set { if (this._timeframe == value) { return; } this._timeframe = value; this.notify_property ("timeframe"); } } [CCode (notify = false)] public GLib.Date date { get { return this._date; } set { if (!value.valid ()) { return; } if (this._date.valid () && this._date.compare (value) == 0) { return; } this._date = value.copy (); this.validate_user_selected_date (); this.notify_property ("date"); } } [CCode (notify = false)] public GLib.Date min_date { get { return this._min_date; } private set { if (this._min_date.valid () && this._min_date.compare (value) == 0) { return; } this._min_date = value; this.notify_property ("min-date"); } } [CCode (notify = false)] public GLib.Date max_date { get { return this._max_date; } private set { if (this._max_date.valid () && this._max_date.compare (value) == 0) { return; } this._max_date = value; this.notify_property ("max-date"); } } private Ft.Timeframe _timeframe = Ft.Timeframe.DAY; private GLib.Date _date; private GLib.Date _min_date; private GLib.Date _max_date; private GLib.Date user_selected_date; private Gom.Repository? repository = null; private Ft.StatsManager? stats_manager = null; private Ft.SleepMonitor? sleep_monitor = null; private GLib.Cancellable? navigation_cancellable = null; private ulong woke_up_id = 0; private ulong entry_saved_id = 0; construct { this.repository = Ft.Database.get_repository (); this.stats_manager = new Ft.StatsManager (); this.sleep_monitor = new Ft.SleepMonitor (); var today = this.stats_manager.get_today (); this.user_selected_date = today.copy (); this._max_date = today.copy (); this._date = today.copy (); this.fetch_min_date.begin ( (obj, res) => { this.min_date = this.fetch_min_date.end (res); }); this.entry_saved_id = this.stats_manager.entry_saved.connect ((entry) => { this.update_date_range (); }); this.woke_up_id = this.sleep_monitor.woke_up.connect (() => { this.update_date_range (); }); } private void validate_user_selected_date () { if (!this.user_selected_date.valid ()) { return; } var is_valid = true; switch (this._timeframe) { case Ft.Timeframe.DAY: is_valid = this.user_selected_date.compare (this._date) == 0; break; case Ft.Timeframe.WEEK: var week_start = this._date.copy (); var week_end = this._date.copy (); week_end.add_days (6U); is_valid = this.user_selected_date.compare (week_start) >= 0 && this.user_selected_date.compare (week_end) <= 0; break; case Ft.Timeframe.MONTH: is_valid = this.user_selected_date.get_month () == this._date.get_month () && this.user_selected_date.get_year () == this._date.get_year (); break; default: assert_not_reached (); } if (!is_valid) { this.user_selected_date = GLib.Date (); } } public void select_date (GLib.Date date) { this.user_selected_date = date.copy (); this.date = this._timeframe.normalize_date (date); } public void select_timeframe (Ft.Timeframe timeframe) { this.timeframe = timeframe; } public void select (GLib.Date date, Ft.Timeframe timeframe) { this.user_selected_date = date.copy (); date = timeframe.normalize_date (date); var date_changed = this._date.valid () ? this._date.compare (date) != 0 : date.valid (); var timeframe_changed = this._timeframe != timeframe; this._timeframe = timeframe; this._date = date; if (timeframe_changed) { this.notify_property ("timeframe"); } if (date_changed) { this.notify_property ("date"); } } public GLib.Date get_selection_date () { return this.user_selected_date.valid () ? this.user_selected_date.copy () : this._date.copy (); } public void update_date_range () { this.max_date = this.stats_manager.get_today (); if (!this.min_date.valid ()) { this.min_date = this.fetch_min_date_sync (); } } private async GLib.Date fetch_min_date () { var min_date = GLib.Date (); if (this.repository == null) { return min_date; } var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.AggregatedStatsEntry), "date", Gom.SortingMode.ASCENDING); try { var results = yield this.repository.find_sorted_async ( typeof (Ft.AggregatedStatsEntry), null, sorting); if (results.count > 0) { yield results.fetch_async (0U, 1U); var entry = (Ft.AggregatedStatsEntry?) results.get_index (0); if (entry != null && entry.date != null) { return Ft.Database.parse_date (entry.date); } } } catch (GLib.Error error) { GLib.critical ("Error while fetching minimal stats date: %s", error.message); } return min_date; } private GLib.Date fetch_min_date_sync () { var min_date = GLib.Date (); if (this.repository == null) { return min_date; } var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.AggregatedStatsEntry), "date", Gom.SortingMode.ASCENDING); try { var results = this.repository.find_sorted_sync ( typeof (Ft.AggregatedStatsEntry), null, sorting); if (results.count > 0) { results.fetch_sync (0U, 1U); var entry = (Ft.AggregatedStatsEntry?) results.get_index (0); if (entry != null && entry.date != null) { return Ft.Database.parse_date (entry.date); } } } catch (GLib.Error error) { GLib.critical ("Error while fetching minimal stats date: %s", error.message); } return min_date; } private async GLib.Date fetch_closest_date (GLib.Date date, NavigationDirection direction) { if (this.repository == null) { return date; } var date_value = GLib.Value (typeof (string)); date_value.set_string (Ft.Database.serialize_date (date)); var category_value = GLib.Value (typeof (string)); category_value.set_string ("pomodoro"); var duration_value = GLib.Value (typeof (int64)); duration_value.set_int64 (Ft.Interval.MINUTE); Gom.Filter date_filter; var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); if (direction == NavigationDirection.FORWARD) { date_filter = new Gom.Filter.gte ( typeof (Ft.AggregatedStatsEntry), "date", date_value); sorting.add ( typeof (Ft.AggregatedStatsEntry), "date", Gom.SortingMode.ASCENDING); } else { date_filter = new Gom.Filter.lte ( typeof (Ft.AggregatedStatsEntry), "date", date_value); sorting.add ( typeof (Ft.AggregatedStatsEntry), "date", Gom.SortingMode.DESCENDING); } var category_filter = new Gom.Filter.eq ( typeof (Ft.AggregatedStatsEntry), "category", category_value); var duration_filter = new Gom.Filter.gte ( typeof (Ft.AggregatedStatsEntry), "duration", duration_value); var filter = new Gom.Filter.and ( new Gom.Filter.and (date_filter, category_filter), duration_filter); try { var results = yield this.repository.find_sorted_async ( typeof (Ft.AggregatedStatsEntry), filter, sorting); if (results.count == 0) { return direction == NavigationDirection.FORWARD ? this._max_date : this._min_date; } yield results.fetch_async (0U, 1U); var entry = (Ft.AggregatedStatsEntry?) results.get_index (0); return entry != null ? Ft.Database.parse_date (entry.date) : date; } catch (GLib.Error error) { GLib.warning ("Error fetching closest date: %s", error.message); return date; } } public async void navigate (NavigationDirection direction, GLib.Cancellable cancellable) { var current_date = this._date.copy (); var date = this._date.copy (); var timeframe = this._timeframe; if (direction == NavigationDirection.FORWARD) { switch (timeframe) { case Ft.Timeframe.DAY: date.add_days (1); break; case Ft.Timeframe.WEEK: date.add_days (7); break; case Ft.Timeframe.MONTH: date.add_months (1); break; default: assert_not_reached (); } } else { date.subtract_days (1); } var closest_date = yield this.fetch_closest_date (date, direction); var skipped = 0U; if (!closest_date.valid () || cancellable.is_cancelled ()) { return; } date = timeframe.normalize_date (closest_date); switch (this._timeframe) { case Ft.Timeframe.DAY: skipped = (uint) (current_date.days_between (date).abs ()); break; case Ft.Timeframe.WEEK: skipped = (uint) ((current_date.days_between (date).abs ()) / 7); break; case Ft.Timeframe.MONTH: var skipped_int = ((int) date.get_year () - (int) current_date.get_year ()) * 12 + ((int) date.get_month () - (int) current_date.get_month ()); skipped = (uint) (skipped_int.abs ()); break; default: assert_not_reached (); } this.user_selected_date = date.copy (); this.date = date.copy (); if (skipped > 1U) { this.navigated (direction, date, skipped - 1U); } else { this.navigated (direction, date, 0U); } } public override void dispose () { if (this.woke_up_id != 0) { this.sleep_monitor.disconnect (this.woke_up_id); this.woke_up_id = 0; } if (this.entry_saved_id != 0) { this.stats_manager.disconnect (this.entry_saved_id); this.entry_saved_id = 0; } this.repository = null; this.stats_manager = null; this.sleep_monitor = null; this.navigation_cancellable = null; base.dispose (); } public signal void navigated (Ft.NavigationDirection direction, GLib.Date date, uint skipped); } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/stats-view.ui")] public class StatsView : Adw.BreakpointBin { private const uint PAGES_HISTORY_LIMIT = 2; private const uint TOAST_DISMISS_TIMEOUT = 2; public int64 daily_interval { get; set; default = Ft.Interval.HOUR; } [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Label title_label; [GtkChild] private unowned Gtk.Label subtitle_label; [GtkChild] private unowned Ft.StatsDatePopover date_popover; [GtkChild] private unowned Gtk.Button up_button; [GtkChild] private unowned Gtk.Stack pages; private Ft.StatsViewModel? model = null; private GLib.SimpleAction? previous_action = null; private GLib.SimpleAction? next_action = null; private GLib.SimpleAction? today_action = null; private GLib.SimpleAction? up_action = null; private GLib.SimpleAction? timeframe_action = null; private GLib.Queue pages_history = null; private GLib.Cancellable? navigation_cancellable = null; private Adw.Toast? last_toast = null; private int64 last_user_active_time = Ft.Timestamp.UNDEFINED; private uint update_page_idle_id = 0U; private uint timeout_id = 0U; static construct { set_css_name ("statsview"); // TODO: move these keybindings to window // currently they work only if view is in focus add_binding_action (Gdk.Key.Left, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.previous", null); add_binding_action (Gdk.Key.Right, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.next", null); add_binding_action (Gdk.Key.Page_Down, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.previous", null); add_binding_action (Gdk.Key.Page_Up, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.next", null); add_binding_action (Gdk.Key.Home, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.today", null); add_binding_action (Gdk.Key.t, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.today", null); add_binding_action (Gdk.Key.d, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.set-timeframe", "s", "day"); add_binding_action (Gdk.Key.w, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.set-timeframe", "s", "week"); add_binding_action (Gdk.Key.m, Gdk.ModifierType.NO_MODIFIER_MASK, "stats.set-timeframe", "s", "month"); } construct { this.model = new Ft.StatsViewModel (); this.pages_history = new GLib.Queue (); this.initialize_action_group (); this.model.bind_property ( "timeframe", this.date_popover, "timeframe", GLib.BindingFlags.SYNC_CREATE); this.model.bind_property ( "date", this.date_popover, "date", GLib.BindingFlags.SYNC_CREATE); this.model.bind_property ( "min-date", this.date_popover, "min-date", GLib.BindingFlags.SYNC_CREATE); this.model.bind_property ( "max-date", this.date_popover, "max-date", GLib.BindingFlags.SYNC_CREATE); this.stack.visible = false; this.model.notify["timeframe"].connect ((pspec) => { this.queue_update_page (); }); this.model.notify["date"].connect ((pspec) => { this.queue_update_page (); }); this.model.notify["min-date"].connect ((pspec) => { this.update_actions (); this.update_placeholder (); this.stack.visible = true; }); this.model.notify["max-date"].connect ((pspec) => { this.update_actions (); this.update_title (); if (this.get_mapped () && this.is_user_idle ()) { this.navigate_to_today (); } }); this.model.navigated.connect ( (direction, date, skipped) => { var last_page = this.pages_history.peek_nth (1U); var transition_type = direction == NavigationDirection.FORWARD ? Gtk.StackTransitionType.SLIDE_LEFT : Gtk.StackTransitionType.SLIDE_RIGHT; if (last_page == null || last_page.date.compare (date) != 0) { if (this.last_toast != null) { this.last_toast.dismiss (); } if (skipped > 0U) { this.notify_skipped_pages (skipped); } } if (this.update_page_idle_id != 0) { this.remove_tick_callback (this.update_page_idle_id); this.update_page_idle_id = 0; } this.update_page (transition_type); }); } private string build_page_name (Ft.Timeframe timeframe, GLib.Date date) { return "%s:%s".printf (timeframe.to_string (), Ft.DateUtils.format_date (date, "%d-%m-%Y")); } private Ft.StatsPage? create_page (Ft.Timeframe timeframe, GLib.Date date) { Ft.StatsPage? page; switch (timeframe) { case Ft.Timeframe.DAY: page = new Ft.StatsDayPage (date); this.bind_property ( "daily-interval", page, "interval", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); break; case Ft.Timeframe.WEEK: page = new Ft.StatsWeekPage (date); break; case Ft.Timeframe.MONTH: page = new Ft.StatsMonthPage (date); break; default: assert_not_reached (); } page.valign = Gtk.Align.START; return page; } private Ft.StatsPage? get_page (string name) { return this.pages.get_child_by_name (name) as Ft.StatsPage; } private Ft.StatsPage get_or_create_page (Ft.Timeframe timeframe, GLib.Date date) { var page_name = this.build_page_name (timeframe, date); var page = this.get_page (page_name); if (page == null) { page = this.create_page (timeframe, date); this.pages.add_named ((Gtk.Widget) page, page_name); } return page; } private void update_page (Gtk.StackTransitionType transition) { if (this.stack.visible_child_name != "content") { return; // showing placeholder } if (!this.get_mapped ()) { return; } var page = this.get_or_create_page (this.model.timeframe, this.model.date); var page_name = this.pages.get_page (page).name; this.pages_history.remove (page); this.pages_history.push_head (page); while (this.pages_history.length > PAGES_HISTORY_LIMIT) { var last_page = this.pages_history.pop_tail (); this.pages.remove (last_page); } this.pages.set_visible_child_full (page_name, transition); this.update_title (); this.update_actions (); this.mark_user_active (); } private void queue_update_page () { if (this.update_page_idle_id != 0) { return; } this.update_page_idle_id = this.add_tick_callback ( () => { this.update_page_idle_id = 0; this.update_page (Gtk.StackTransitionType.CROSSFADE); return GLib.Source.REMOVE; }); } private void clear_pages () { while (this.pages_history.length > 0) { var last_page = this.pages_history.pop_tail (); this.pages.remove (last_page); } } private void notify_skipped_pages (uint count) { string message; switch (this.model.timeframe) { case Ft.Timeframe.DAY: message = ngettext ("Skipped %u day", "Skipped %u days", count).printf (count); break; case Ft.Timeframe.WEEK: message = ngettext ("Skipped %u week", "Skipped %u weeks", count).printf (count); break; case Ft.Timeframe.MONTH: message = ngettext ("Skipped %u month", "Skipped %u months", count).printf (count); break; default: assert_not_reached (); } var toast = new Adw.Toast (message); toast.use_markup = false; toast.priority = Adw.ToastPriority.NORMAL; toast.timeout = TOAST_DISMISS_TIMEOUT; toast.dismissed.connect (() => { this.last_toast = null; }); var window = this.get_root () as Ft.Window; if (window == null) { GLib.warning ("Unable to show a toast '%s'", message); return; } window.add_toast (toast); this.last_toast = toast; } private void navigate_to_today () { this.model.select_date (this.model.max_date); } private void navigate_up () { var timeframe = this.model.timeframe; var date = this.model.get_selection_date (); switch (timeframe) { case Ft.Timeframe.DAY: timeframe = Ft.Timeframe.WEEK; break; case Ft.Timeframe.WEEK: timeframe = Ft.Timeframe.MONTH; break; case Ft.Timeframe.MONTH: return; default: assert_not_reached (); } this.model.select (date, timeframe); } private void activate_previous (GLib.SimpleAction action, GLib.Variant? parameter) { if (this.navigation_cancellable != null) { this.navigation_cancellable.cancel (); } this.navigation_cancellable = new GLib.Cancellable (); this.model.navigate.begin (NavigationDirection.BACKWARD, this.navigation_cancellable); } private void activate_next (GLib.SimpleAction action, GLib.Variant? parameter) { if (this.navigation_cancellable != null) { this.navigation_cancellable.cancel (); } this.navigation_cancellable = new GLib.Cancellable (); this.model.navigate.begin (NavigationDirection.FORWARD, this.navigation_cancellable); } private void activate_today (GLib.SimpleAction action, GLib.Variant? parameter) { this.navigate_to_today (); } private void activate_up () { this.navigate_up (); } private void activate_timeframe (GLib.SimpleAction action, GLib.Variant? parameter) { this.model.select_timeframe (Ft.Timeframe.from_string (parameter.get_string ())); } private void activate_set_timeframe (GLib.SimpleAction action, GLib.Variant? parameter) { if (parameter != null) { this.timeframe_action.activate (parameter); } } private void activate_select_day_action (GLib.SimpleAction action, GLib.Variant? parameter) { if (parameter == null) { return; } var date = Ft.DateUtils.date_from_variant (parameter); if (date.valid ()) { this.model.select (date, Ft.Timeframe.DAY); } } private void initialize_action_group () { var previous_action = new GLib.SimpleAction ("previous", null); previous_action.activate.connect (this.activate_previous); var next_action = new GLib.SimpleAction ("next", null); next_action.activate.connect (this.activate_next); var today_action = new GLib.SimpleAction ("today", null); today_action.activate.connect (this.activate_today); var up_action = new GLib.SimpleAction ("up", null); up_action.bind_property ("enabled", this.up_button, "visible", GLib.BindingFlags.SYNC_CREATE); up_action.activate.connect (this.activate_up); var timeframe_action = new GLib.SimpleAction.stateful ( "timeframe", GLib.VariantType.STRING, new GLib.Variant.string (this.model.timeframe.to_string ())); timeframe_action.activate.connect (this.activate_timeframe); var set_timeframe_action = new GLib.SimpleAction ( "set-timeframe", GLib.VariantType.STRING); set_timeframe_action.activate.connect (this.activate_set_timeframe); var select_day_action = new GLib.SimpleAction ( "select-day", GLib.VariantType.TUPLE); select_day_action.activate.connect (this.activate_select_day_action); var action_group = new GLib.SimpleActionGroup (); action_group.add_action (previous_action); action_group.add_action (next_action); action_group.add_action (today_action); action_group.add_action (up_action); action_group.add_action (timeframe_action); action_group.add_action (set_timeframe_action); action_group.add_action (select_day_action); this.previous_action = previous_action; this.next_action = next_action; this.today_action = today_action; this.up_action = up_action; this.timeframe_action = timeframe_action; this.insert_action_group ("stats", action_group); } private bool is_user_idle () { var now = Ft.Timestamp.from_now (); return Ft.Timestamp.is_defined (this.last_user_active_time) ? now - this.last_user_active_time > Ft.Interval.HOUR : false; } private void mark_user_active () { this.last_user_active_time = Ft.Timestamp.from_now (); } private void update_title () { string title; string subtitle; var timeframe = this.model.timeframe; var date = this.model.date; var today = timeframe.normalize_date (this.model.max_date); switch (timeframe) { case Ft.Timeframe.DAY: subtitle = capitalize_words (Ft.DateUtils.format_date (date, "%A")); if (date.compare (today) == 0) { title = _("Today"); subtitle += capitalize_words ( Ft.DateUtils.format_date (date, ", %e %B").replace (" ", "")); } else if (date.days_between (today) == 1) { title = _("Yesterday"); subtitle += capitalize_words ( Ft.DateUtils.format_date (date, ", %e %B").replace (" ", "")); } else if (date.get_year () == today.get_year ()) { title = capitalize_words ( Ft.DateUtils.format_date (date, "%e %B").replace (" ", "")); } else { title = capitalize_words ( Ft.DateUtils.format_date (date, "%e %B %Y").replace (" ", "")); } break; case Ft.Timeframe.WEEK: var week_start_date = date.copy (); var week_end_date = week_start_date.copy (); week_end_date.add_days (6U); if (week_start_date.compare (today) == 0) { title = _("This week"); } else { var first_day_of_week = Ft.Locale.get_first_day_of_week (); var first_week_date = GLib.Date (); first_week_date.set_dmy (1, 1, week_end_date.get_year ()); var week_number_offset = 1U - ( first_day_of_week == GLib.DateWeekday.MONDAY ? first_week_date.get_monday_week_of_year () : first_week_date.get_sunday_week_of_year () ); var week_number = week_number_offset + ( first_day_of_week == GLib.DateWeekday.MONDAY ? week_end_date.get_monday_week_of_year () : week_end_date.get_sunday_week_of_year () ); var year = (uint) week_end_date.get_year (); title = week_end_date.get_year () == today.get_year () ? _("Week %u").printf (week_number) : _("Week %u of %u").printf (week_number, year); } var week_start_format = "%e"; var week_end_format = "%e %B"; if (week_start_date.get_month () != week_end_date.get_month ()) { week_start_format += " %B"; } if (week_end_date.get_year () != today.get_year ()) { week_end_format += " %Y"; } subtitle = capitalize_words ("%s – %s".printf ( Ft.DateUtils.format_date (week_start_date, week_start_format).replace (" ", ""), Ft.DateUtils.format_date (week_end_date, week_end_format).replace (" ", ""))); break; case Ft.Timeframe.MONTH: title = capitalize_words ( Ft.DateUtils.get_month_name (date.get_month ())); if (date.get_year () != today.get_year ()) { title += Ft.DateUtils.format_date (date, " %Y"); } subtitle = ""; break; default: assert_not_reached (); } this.title_label.label = title; this.subtitle_label.label = subtitle; this.subtitle_label.visible = subtitle != ""; } private void update_actions () { var timeframe = this.model.timeframe; var date = this.model.date; var min_date = timeframe.normalize_date (this.model.min_date); var max_date = timeframe.normalize_date (this.model.max_date); this.previous_action.set_enabled (min_date.valid () && date.compare (min_date) > 0); this.next_action.set_enabled (max_date.valid () && date.compare (max_date) < 0); this.today_action.set_enabled (this.next_action.get_enabled ()); this.up_action.set_enabled (timeframe != Ft.Timeframe.MONTH); this.timeframe_action.set_state (new GLib.Variant.string (timeframe.to_string ())); } private void schedule_navigate_to_today () { if (this.timeout_id != 0) { return; } // Check date on full hour var now = new GLib.DateTime.now_local (); var seconds = 3600 - (now.get_minute () * 60 + now.get_second ()); this.timeout_id = GLib.Timeout.add_seconds ( seconds + 1, () => { this.timeout_id = 0; this.model.update_date_range (); this.schedule_navigate_to_today (); if (this.is_user_idle ()) { this.navigate_to_today (); } return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.timeout_id, "Ft.StatsView.navigate_to_today"); } private void update_placeholder () { if (this.model.min_date.valid ()) { this.stack.set_visible_child_full ("content", Gtk.StackTransitionType.NONE); this.update_page (Gtk.StackTransitionType.NONE); } else { this.stack.set_visible_child_full ("placeholder", Gtk.StackTransitionType.NONE); this.clear_pages (); } } [GtkCallback] private void on_timeframe_selected (Ft.StatsDatePopover date_popover, Ft.Timeframe timeframe) { this.model.select_timeframe (timeframe); GLib.Signal.stop_emission_by_name (date_popover, "timeframe-selected"); } [GtkCallback] private void on_date_selected (Ft.StatsDatePopover date_popover, GLib.Date date) { this.model.select_date (date); GLib.Signal.stop_emission_by_name (date_popover, "date-selected"); } public override void map () { this.model.update_date_range (); base.map (); if (this.model.min_date.valid ()) { this.update_placeholder (); if (this.is_user_idle ()) { this.navigate_to_today (); } else { this.mark_user_active (); } } this.schedule_navigate_to_today (); } public override void unmap () { base.unmap (); if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } } public override void dispose () { if (this.update_page_idle_id != 0) { this.remove_tick_callback (this.update_page_idle_id); this.update_page_idle_id = 0; } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.clear_pages (); this.previous_action = null; this.next_action = null; this.today_action = null; this.up_action = null; this.timeframe_action = null; this.pages_history = null; this.navigation_cancellable = null; this.last_toast = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-week-page.ui000066400000000000000000000056501520625676500255130ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/stats-week-page.vala000066400000000000000000000253461520625676500260250ustar00rootroot00000000000000/* * Copyright (c) 2017-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/stats-week-page.ui")] public class StatsWeekPage : Adw.Bin, Ft.StatsPage { public GLib.Date date { get; construct; } [GtkChild] private unowned Ft.BarChart histogram; [GtkChild] private unowned Ft.StatsCard pomodoro_card; [GtkChild] private unowned Ft.StatsCard breaks_card; [GtkChild] private unowned Ft.StatsCard interruptions_card; [GtkChild] private unowned Ft.StatsCard break_ratio_card; private Ft.StatsManager? stats_manager; private GLib.Date start_date; private GLib.Date end_date; construct { this.stats_manager = new Ft.StatsManager (); this.stats_manager.entry_saved.connect (this.on_entry_saved); this.stats_manager.entry_deleted.connect (this.on_entry_deleted); this.histogram.set_format_value_func (format_hours); this.histogram.set_category_label ( Ft.StatsCategory.POMODORO, _("Pomodoro")); this.histogram.set_category_unit ( Ft.StatsCategory.POMODORO, Ft.Unit.INTERVAL); this.histogram.set_category_label ( Ft.StatsCategory.BREAK, _("Breaks")); this.histogram.set_category_unit ( Ft.StatsCategory.BREAK, Ft.Unit.INTERVAL); this.histogram.set_category_label ( Ft.StatsCategory.INTERRUPTION, _("Interruptions")); this.histogram.set_category_unit ( Ft.StatsCategory.INTERRUPTION, Ft.Unit.AMOUNT); this.histogram.set_category_visible ( Ft.StatsCategory.INTERRUPTION, false); this.start_date = Ft.Timeframe.WEEK.normalize_date (this.date); this.end_date = this.start_date.copy (); this.end_date.add_days (6U); this.populate.begin ( (obj, res) => { this.populate.end (res); }); } public StatsWeekPage (GLib.Date date) { GLib.Object ( date: date ); } private static string format_hours (double value) { return Ft.Interval.format_short (Ft.Interval.from_seconds (value), Ft.Interval.HOUR); } private GLib.Date transform_position (uint bar_index) { var date = this.start_date.copy (); date.add_days (bar_index); return date; } private void reset () { this.histogram.fill (0.0); this.pomodoro_card.value = 0.0; this.breaks_card.value = 0.0; this.interruptions_card.value = 0.0; this.break_ratio_card.value = double.NAN; } private void update_category_colors () { var foreground_color = this.histogram.get_color (); var pomodoro_color = Ft.get_chart_primary_color (foreground_color); var break_color = Ft.get_chart_secondary_color (foreground_color); this.histogram.set_category_color (Ft.StatsCategory.POMODORO, pomodoro_color); this.histogram.set_category_color (Ft.StatsCategory.BREAK, break_color); } private void update_histogram_labels () { var date = this.start_date.copy (); for (var bar_index = 0; bar_index <= 6; bar_index++) { var bar_label = capitalize_words ( Ft.DateUtils.format_date (date, "%a")); var tooltip_label = capitalize_words ( Ft.DateUtils.format_date (date, "%e %B")); this.histogram.set_bar_label (bar_index, bar_label, tooltip_label); date.add_days (1U); } } private async Gom.ResourceGroup? fetch_aggregated_entries () { var repository = Ft.Database.get_repository (); var start_date_value = GLib.Value (typeof (string)); start_date_value.set_string (Ft.Database.serialize_date (this.start_date)); var end_date_value = GLib.Value (typeof (string)); end_date_value.set_string (Ft.Database.serialize_date (this.end_date)); var start_date_filter = new Gom.Filter.gte ( typeof (Ft.AggregatedStatsEntry), "date", start_date_value); var end_date_filter = new Gom.Filter.lte ( typeof (Ft.AggregatedStatsEntry), "date", end_date_value); var date_filter = new Gom.Filter.and (start_date_filter, end_date_filter); try { var aggregated_entries = yield repository.find_async ( typeof (Ft.AggregatedStatsEntry), date_filter); yield aggregated_entries.fetch_async (0U, aggregated_entries.count); return aggregated_entries; } catch (GLib.Error error) { GLib.critical ("Error while fetching weekly stats: %s", error.message); return null; } } private void process_aggregated_entry (Ft.AggregatedStatsEntry entry) { var entry_category = Ft.StatsCategory.from_string (entry.category); var entry_date = Ft.Database.parse_date (entry.date); var entry_duration = entry.duration; // Validate if entry is relevant if (entry_category == Ft.StatsCategory.INVALID) { return; } // Update histogram var bar_index = (uint) int.max (this.start_date.days_between (entry_date), 0); var category_index = (int) entry_category; var bucket_value = entry_category != Ft.StatsCategory.INTERRUPTION ? Ft.Interval.to_seconds (entry_duration) : (double) entry.count; this.histogram.add_value (bar_index, category_index, bucket_value); // Update cards switch (entry_category) { case Ft.StatsCategory.POMODORO: this.pomodoro_card.value += bucket_value; break; case Ft.StatsCategory.BREAK: this.breaks_card.value += bucket_value; break; case Ft.StatsCategory.INTERRUPTION: this.interruptions_card.value += bucket_value; break; default: // no matching card in UI break; } var total = this.pomodoro_card.value + this.breaks_card.value; this.break_ratio_card.value = total >= 3600.0 ? this.breaks_card.value / total : double.NAN; } private void process_entry (Ft.StatsEntry entry, int sign = 1) requires (sign == 1 || sign == -1) { var entry_category = Ft.StatsCategory.from_string (entry.category); var entry_date = Ft.Database.parse_date (entry.date); var entry_duration = entry.duration; // Validate if entry is relevant if (entry_category == Ft.StatsCategory.INVALID) { return; } // Validate date range if (entry_date.compare (this.start_date) < 0 || entry_date.compare (this.end_date) > 0) { return; } // Update histogram var bar_index = (uint) int.max (this.start_date.days_between (entry_date), 0); var category_index = (int) entry_category; var bucket_value = entry_category != Ft.StatsCategory.INTERRUPTION ? Ft.Interval.to_seconds (sign * entry_duration) : (double) sign; this.histogram.add_value (bar_index, category_index, bucket_value); // Update cards switch (entry_category) { case Ft.StatsCategory.POMODORO: this.pomodoro_card.value += bucket_value; break; case Ft.StatsCategory.BREAK: this.breaks_card.value += bucket_value; break; case Ft.StatsCategory.INTERRUPTION: this.interruptions_card.value += bucket_value; break; default: // no matching card in UI break; } var total = this.pomodoro_card.value + this.breaks_card.value; this.break_ratio_card.value = total >= 3600.0 ? this.breaks_card.value / total : double.NAN; } private async void populate () { var aggregated_entries = yield this.fetch_aggregated_entries (); this.update_histogram_labels (); this.reset (); for (var index = 0U; index < aggregated_entries.count; index++) { this.process_aggregated_entry ( (Ft.AggregatedStatsEntry) aggregated_entries.get_index (index)); } } private void on_entry_saved (Ft.StatsEntry entry) { this.process_entry (entry, 1); } private void on_entry_deleted (Ft.StatsEntry entry) { this.process_entry (entry, -1); } [GtkCallback] private void on_bar_activated (uint bar_index) { var date = this.transform_position (bar_index); this.activate_action_variant ("stats.select-day", Ft.DateUtils.date_to_variant (date)); } public override void css_changed (Gtk.CssStyleChange change) { base.css_changed (change); this.update_category_colors (); } public override void dispose () { this.histogram.set_format_value_func (null); if (this.stats_manager != null) { this.stats_manager.entry_saved.disconnect (this.on_entry_saved); this.stats_manager.entry_deleted.disconnect (this.on_entry_deleted); this.stats_manager = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/000077500000000000000000000000001520625676500236135ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/day-chooser.ui000066400000000000000000000042661520625676500263770ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/day-chooser.vala000066400000000000000000000270751520625676500267100ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/widgets/day-chooser.ui")] public sealed class DayChooser : Adw.Bin { private const int DAYS_PER_WEEK = 7; public GLib.Date selected_date { get { return this._selected_date; } set { this._selected_date = value.copy (); if (!this._selected_date.valid ()) { this.update_selection (); return; } var display_month = this._selected_date.get_month (); var display_year = this._selected_date.get_year (); if (this._display_month != display_month || this._display_year != display_year) { this.set_display_month_year (display_month, display_year); } else { this.update_selection (); } } } public GLib.Date min_date { get { return this._min_date; } set { this._min_date = value.copy (); this.update_previous_button (); } } public GLib.Date max_date { get { return this._max_date; } set { this._max_date = value.copy (); this.update_next_button (); } } public GLib.DateMonth display_month { get { return this._display_month; } } public GLib.DateYear display_year { get { return this._display_year; } } [GtkChild] private unowned Gtk.Label header_label; [GtkChild] private unowned Gtk.Button previous_button; [GtkChild] private unowned Gtk.Button next_button; [GtkChild] private unowned Gtk.Grid grid; private GLib.Date _selected_date; private GLib.Date _min_date; private GLib.Date _max_date; private GLib.DateMonth _display_month; private GLib.DateYear _display_year; private unowned Gtk.Widget selected_button = null; private GLib.Date display_start_date; private GLib.Date display_end_date; static construct { set_css_name ("calendar"); } private void set_display_month_year (GLib.DateMonth month, GLib.DateYear year) { if (this._display_month == month && this._display_year == year) { return; } var month_start_date = GLib.Date (); month_start_date.set_dmy (1, month, year); var month_end_date = GLib.Date (); month_end_date.set_dmy (GLib.Date.get_days_in_month (month, year), month, year); this._display_month = month; this._display_year = year; this.display_start_date = Ft.Timeframe.WEEK.normalize_date (month_start_date); this.display_end_date = Ft.Timeframe.WEEK.normalize_date (month_end_date); this.display_end_date.add_days (6U); this.update (); } private Gtk.Button create_day_button (GLib.Date date) { var button = new Gtk.Button (); button.label = date.get_day ().to_string (); button.add_css_class ("circular"); button.add_css_class ("flat"); button.add_css_class ("day"); button.width_request = 32; button.height_request = 32; unowned var self = this; var date_copy = date.copy (); button.clicked.connect ( () => { self.select (date_copy); }); return button; } private void clear_weekdays () { var child = this.grid.get_first_child (); int row; while (child != null) { this.grid.query_child (child, null, out row, null, null); if (row == 0) { var next_child = child.get_next_sibling (); this.grid.remove (child); child = next_child; } else { child = child.get_next_sibling (); } } } private void clear_days () { var child = this.grid.get_first_child (); int row; while (child != null) { this.grid.query_child (child, null, out row, null, null); if (row != 0) { var next_child = child.get_next_sibling (); this.grid.remove (child); child = next_child; } else { child = child.get_next_sibling (); } } this.selected_button = null; } /** * Place weekday labels as first row of the grid. */ private void update_weekday_labels () { this.clear_weekdays (); var date = this.display_start_date.copy (); for (var column = 0; column < DAYS_PER_WEEK; column++) { var day_name = Ft.DateUtils.format_date (date, "%a"); var day_letter = day_name.get_char (0).toupper ().to_string (); var label = new Gtk.Label (day_letter); label.add_css_class ("dim-label"); label.add_css_class ("weekday"); label.xalign = 0.5f; label.yalign = 0.5f; this.grid.attach (label, column, 0, 1, 1); date.add_days (1U); } } private void update_previous_button () { this.previous_button.sensitive = !this._min_date.valid () || this._display_month > this._min_date.get_month () || this._display_year > this._min_date.get_year (); } private void update_next_button () { this.next_button.sensitive = !this._max_date.valid () || this._display_month < this._max_date.get_month () || this._display_year < this._max_date.get_year (); } private void update_header () { var month_name = capitalize_words (Ft.DateUtils.get_month_name (this._display_month)); var year = (int) this._display_year; this.header_label.label = @"$(month_name) $(year)"; this.update_previous_button (); this.update_next_button (); } private void create_days () { var date = this.display_start_date.copy (); var is_min_date_valid = this._min_date.valid (); var is_max_date_valid = this._max_date.valid (); var row = 1; var column = 0; while (date.compare (this.display_end_date) <= 0) { var button = this.create_day_button (date); button.sensitive = (!is_min_date_valid || this._min_date.compare (date) <= 0) && (!is_max_date_valid || this._max_date.compare (date) >= 0); if (date.get_month () != this._display_month) { button.add_css_class ("dim-label"); } this.grid.attach (button, column, row, 1, 1); date.add_days (1U); column++; if (column >= DAYS_PER_WEEK) { column = 0; row++; } } } private void update_days () { if (!this.get_mapped ()) { return; } this.clear_days (); this.create_days (); } private void update_selection () { if (this.selected_button != null) { this.selected_button.unset_state_flags (Gtk.StateFlags.SELECTED); this.selected_button = null; } if (this.display_start_date.valid () && this._selected_date.valid ()) { var position = this.display_start_date.days_between (this._selected_date); var column = position % DAYS_PER_WEEK; var row = 1 + (position - column) / DAYS_PER_WEEK; this.selected_button = this.grid.get_child_at (column, row); this.selected_button?.set_state_flags (Gtk.StateFlags.SELECTED, false); } } private void update () { this.update_header (); this.update_weekday_labels (); this.update_days (); this.update_selection (); } [GtkCallback] private void on_previous_button_clicked () { var month = this._display_month; var year = this._display_year; if (month == GLib.DateMonth.JANUARY) { year--; month = GLib.DateMonth.DECEMBER; } else { month = (GLib.DateMonth)(Ft.DateUtils.get_month_number (month) - 1U); } this.set_display_month_year (month, year); } [GtkCallback] private void on_next_button_clicked () { var month = this._display_month; var year = this._display_year; if (month == GLib.DateMonth.DECEMBER) { year++; month = GLib.DateMonth.JANUARY; } else { month = (GLib.DateMonth)(Ft.DateUtils.get_month_number (month) + 1U); } this.set_display_month_year (month, year); } private bool select (GLib.Date date) { if (!date.valid ()) { return false; } if (this._selected_date.valid () && this._selected_date.compare (date) == 0) { return true; } this.selected_date = date; this.selected (this._selected_date); return true; } public void reset () { if (this._selected_date.valid ()) { this.set_display_month_year (this._selected_date.get_month (), this._selected_date.get_year ()); } else { var today = Ft.DateUtils.get_today (); this.set_display_month_year (today.get_month (), today.get_year ()); } } public override void map () { this.reset (); this.create_days (); this.update_selection (); base.map (); } public override void unmap () { base.unmap (); // HACK: CSS animations kick in when widgets gets mapped, // to avoid them just remove children this.clear_days (); } public signal void selected (GLib.Date date); public override void dispose () { this.selected_button = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/month-chooser.ui000066400000000000000000000042701520625676500267420ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/month-chooser.vala000066400000000000000000000172471520625676500272600ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/widgets/month-chooser.ui")] public sealed class MonthChooser : Adw.Bin { private const int MONTHS_PER_ROW = 3; public GLib.Date selected_date { get { return this._selected_date; } set { this._selected_date = value.copy (); if (!this._selected_date.valid ()) { this.update_selection (); return; } var display_year = this._selected_date.get_year (); if (this._display_year != display_year) { this.set_display_year (display_year); } else { this.update_selection (); } } } public GLib.Date min_date { get { return this._min_date; } set { this._min_date = value.copy (); this.update_previous_button (); } } public GLib.Date max_date { get { return this._max_date; } set { this._max_date = value.copy (); this.update_next_button (); } } public GLib.DateYear display_year { get { return this._display_year; } } [GtkChild] private unowned Gtk.Label header_label; [GtkChild] private unowned Gtk.Button previous_button; [GtkChild] private unowned Gtk.Button next_button; [GtkChild] private unowned Gtk.Grid grid; private GLib.Date _selected_date; private GLib.Date _min_date; private GLib.Date _max_date; private unowned Gtk.Widget selected_button = null; private GLib.DateYear _display_year; static construct { set_css_name ("calendar"); } private void set_display_year (GLib.DateYear year) { if (this._display_year == year) { return; } this._display_year = year; this.update (); } private Gtk.Button create_month_button (GLib.Date date) { var button = new Gtk.Button (); button.label = capitalize_words (Ft.DateUtils.format_date (date, "%b")); button.add_css_class ("pill"); button.add_css_class ("flat"); button.add_css_class ("month"); unowned var self = this; var date_copy = date.copy (); button.clicked.connect ( () => { self.select (date_copy); }); return button; } private void clear_months () { var child = this.grid.get_first_child (); while (child != null) { var next_child = child.get_next_sibling (); this.grid.remove (child); child = next_child; } this.selected_button = null; } private void update_previous_button () { this.previous_button.sensitive = !this._min_date.valid () || this._display_year > this._min_date.get_year (); } private void update_next_button () { this.next_button.sensitive = !this._max_date.valid () || this._display_year < this._max_date.get_year (); } private void update_header () { var year = (int) this._display_year; this.header_label.label = @"$(year)"; this.update_previous_button (); this.update_next_button (); } private void create_months () { var date = GLib.Date (); date.set_dmy (1, 1, this._display_year); var is_min_date_valid = this._min_date.valid (); var is_max_date_valid = this._max_date.valid (); var row = 0; var column = 0; for (var month = 1; month <= 12; month++) { var button = this.create_month_button (date); button.sensitive = (!is_min_date_valid || this._min_date.compare (date) <= 0) && (!is_max_date_valid || this._max_date.compare (date) >= 0); this.grid.attach (button, column, row, 1, 1); date.add_months (1U); column++; if (column >= MONTHS_PER_ROW) { column = 0; row++; } } } private void update_months () { if (!this.get_mapped ()) { return; } this.clear_months (); this.create_months (); } private void update_selection () { if (this.selected_button != null) { this.selected_button.unset_state_flags (Gtk.StateFlags.SELECTED); this.selected_button = null; } if (this._selected_date.valid () && this._selected_date.get_year () == this._display_year) { var position = (int) this._selected_date.get_month () - 1; var column = position % MONTHS_PER_ROW; var row = (position - column) / MONTHS_PER_ROW; this.selected_button = this.grid.get_child_at (column, row); this.selected_button?.set_state_flags (Gtk.StateFlags.SELECTED, false); } } private void update () { this.update_header (); this.update_months (); this.update_selection (); } [GtkCallback] private void on_previous_button_clicked () { this.set_display_year (this._display_year - 1); } [GtkCallback] private void on_next_button_clicked () { this.set_display_year (this._display_year + 1); } private bool select (GLib.Date date) { if (!date.valid ()) { return false; } if (this._selected_date.valid () && this._selected_date.compare (date) == 0) { return true; } this.selected_date = date; this.selected (this._selected_date); return true; } public void reset () { if (this._selected_date.valid ()) { this.set_display_year (this._selected_date.get_year ()); } else { var today = Ft.DateUtils.get_today (); this.set_display_year (today.get_year ()); } } public override void map () { this.reset (); this.create_months (); this.update_selection (); base.map (); } public override void unmap () { base.unmap (); // HACK: CSS animations kick in when widgets gets mapped, // to avoid them just remove children this.clear_months (); } public signal void selected (GLib.Date date); public override void dispose () { this.selected_button = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/stats-card.ui000066400000000000000000000017021520625676500262170ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/stats-card.vala000066400000000000000000000021301520625676500265210ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/widgets/stats-card.ui")] public class StatsCard : Adw.Bin { public string label { get; set; } public Ft.Unit unit { get { return this._unit; } set { this._unit = value; this.update (); } } public double value { get { return this._value; } set { this._value = value; this.update (); } } [GtkChild] private unowned Gtk.Label value_label; private Ft.Unit _unit = Ft.Unit.AMOUNT; private double _value = double.NAN; static construct { set_css_name ("statscard"); } private void update () { this.value_label.label = this._unit.format (this._value); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/stats-date-popover.ui000066400000000000000000000064551520625676500277250ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/stats-date-popover.vala000066400000000000000000000344071520625676500302310ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/widgets/stats-date-popover.ui")] public class StatsDatePopover : Gtk.Popover { [CCode (notify = false)] public Ft.Timeframe timeframe { get { return this._timeframe; } set { var date = this.refine_date (this._date, this._timeframe); this.set_date_timeframe (date, value); } } [CCode (notify = false)] public GLib.Date date { get { return this._date; } set { if (!value.valid ()) { return; } this.set_date_timeframe (value, this._timeframe); } } public GLib.Date min_date { get { return this._min_date; } set { if (this._min_date.valid () && this._min_date.compare (value) == 0) { return; } this._min_date = value; this.update_date_range (); } } public GLib.Date max_date { get { return this._max_date; } set { if (this._max_date.valid () && this._max_date.compare (value) == 0) { return; } this._max_date = value; this.update_date_range (); } } [GtkChild] private unowned Adw.ToggleGroup timeframe_toggle_group; [GtkChild] private unowned Ft.DayChooser day_chooser; [GtkChild] private unowned Ft.WeekChooser week_chooser; [GtkChild] private unowned Ft.MonthChooser month_chooser; private Ft.Timeframe _timeframe = Ft.Timeframe.DAY; private GLib.Date _date; private GLib.Date _min_date; private GLib.Date _max_date; private GLib.Date user_selected_date; construct { this.bind_property ( "timeframe", this.timeframe_toggle_group, "active-name", GLib.BindingFlags.SYNC_CREATE, transform_from_timeframe, transform_to_timeframe); } private static bool transform_from_timeframe (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var timeframe = (Ft.Timeframe) source_value.get_enum (); target_value.set_string (timeframe.to_string ()); return true; } private static bool transform_to_timeframe (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var timeframe = Ft.Timeframe.from_string (source_value.get_string ()); target_value.set_enum (timeframe); return true; } private void get_display_month_year (out GLib.DateMonth display_month, out GLib.DateYear display_year) { switch (this._timeframe) { case Ft.Timeframe.DAY: display_month = this.day_chooser.display_month; display_year = this.day_chooser.display_year; break; case Ft.Timeframe.WEEK: display_month = this.week_chooser.display_month; display_year = this.week_chooser.display_year; break; case Ft.Timeframe.MONTH: display_month = this._date.valid () ? this._date.get_month () : GLib.DateMonth.BAD_MONTH; display_year = this.month_chooser.display_year; break; default: assert_not_reached (); } if (!display_month.valid () || !display_year.valid ()) { var today = Ft.DateUtils.get_today (); display_month = today.get_month (); display_year = today.get_year (); } } private void update_selection (GLib.DateMonth display_month, GLib.DateYear display_year) { if (!this._date.valid ()) { this.day_chooser.selected_date = GLib.Date (); this.week_chooser.selected_date = GLib.Date (); this.month_chooser.selected_date = GLib.Date (); return; } this.day_chooser.selected_date = this._timeframe == Ft.Timeframe.DAY ? Ft.Timeframe.DAY.normalize_date (this._date) : GLib.Date (); this.month_chooser.selected_date = this._timeframe == Ft.Timeframe.MONTH ? Ft.Timeframe.MONTH.normalize_date (this._date) : GLib.Date (); if (this._timeframe == Ft.Timeframe.WEEK) { var week_start_date = Ft.Timeframe.WEEK.normalize_date (this._date); var week_end_date = week_start_date.copy (); week_end_date.add_days (6U); var display_date = GLib.Date (); display_date.set_dmy (1, display_month, display_year); if (week_start_date.compare (display_date) >= 0) { this.week_chooser.set_selected_date_full ( Ft.Timeframe.WEEK.normalize_date (this._date), week_start_date.get_month (), week_start_date.get_year ()); } else { this.week_chooser.set_selected_date_full ( Ft.Timeframe.WEEK.normalize_date (this._date), week_end_date.get_month (), week_end_date.get_year ()); } } else { this.week_chooser.selected_date = GLib.Date (); } } private void update_date_range () { this.day_chooser.min_date = Ft.Timeframe.DAY.normalize_date (this._min_date); this.day_chooser.max_date = Ft.Timeframe.DAY.normalize_date (this._max_date); this.week_chooser.min_date = Ft.Timeframe.WEEK.normalize_date (this._min_date); this.week_chooser.max_date = Ft.Timeframe.WEEK.normalize_date (this._max_date); this.month_chooser.min_date = Ft.Timeframe.MONTH.normalize_date (this._min_date); this.month_chooser.max_date = Ft.Timeframe.MONTH.normalize_date (this._max_date); } private void set_date_timeframe (GLib.Date date, Ft.Timeframe timeframe) { GLib.DateMonth display_month; GLib.DateYear display_year; this.get_display_month_year (out display_month, out display_year); date = timeframe.normalize_date (date); var date_changed = this._date.valid () ? this._date.compare (date) != 0 : date.valid (); var timeframe_changed = this._timeframe != timeframe; this._timeframe = timeframe; this._date = date; if (timeframe_changed || date_changed) { this.update_selection (display_month, display_year); } if (timeframe_changed) { this.notify_property ("timeframe"); } if (date_changed) { this.notify_property ("date"); } } /** * Refinement tries to recover lost resolution at higher timeframes. * * When selecting a higher timeframes we may loose user intent. We want to transition * between "this month", "this week" and "today" while switching timeframes. */ private GLib.Date refine_date (GLib.Date date, Ft.Timeframe date_timeframe, GLib.DateMonth display_month = GLib.DateMonth.BAD_MONTH, GLib.DateYear display_year = GLib.DateYear.BAD_YEAR) { var today = Ft.DateUtils.get_today (); var refined_date = date.copy (); if (!date.valid ()) { return this.user_selected_date.valid () ? this.user_selected_date : today; } switch (date_timeframe) { case Ft.Timeframe.DAY: break; case Ft.Timeframe.WEEK: var week_start_date = date.copy (); var week_end_date = date.copy (); week_end_date.add_days (6U); var display_date = GLib.Date (); if (display_month.valid () && display_year.valid ()) { display_date.set_dmy (1, display_month, display_year); } if (display_date.valid () && week_start_date.get_month () != week_end_date.get_month ()) { if (display_date.compare (week_start_date) <= 0) { var month = week_start_date.get_month (); var year = week_start_date.get_year (); week_end_date.set_dmy ( GLib.Date.get_days_in_month (month, year), month, year); } else { week_start_date.set_dmy ( 1, week_end_date.get_month (), week_end_date.get_year ()); } } if (this.user_selected_date.valid () && this.user_selected_date.compare (week_start_date) >= 0 && this.user_selected_date.compare (week_end_date) <= 0) { refined_date = this.user_selected_date.copy (); } else if (today.compare (week_start_date) >= 0 && today.compare (week_end_date) <= 0) { refined_date = today.copy (); } else if (date.compare (week_start_date) < 0) { refined_date = week_start_date.copy (); } else if (date.compare (week_end_date) > 0) { refined_date = week_end_date.copy (); } break; case Ft.Timeframe.MONTH: if (this.user_selected_date.valid () && this.user_selected_date.get_month () == date.get_month () && this.user_selected_date.get_year () == date.get_year ()) { refined_date = this.user_selected_date.copy (); } else if (date.get_month () == today.get_month () && date.get_year () == today.get_year ()) { refined_date = today.copy (); } break; default: assert_not_reached (); } if (this._min_date.valid () && this._min_date.compare (refined_date) > 0) { refined_date = this._min_date.copy (); } if (this._max_date.valid () && this._max_date.compare (refined_date) < 0) { refined_date = this._max_date.copy (); } return refined_date; } [GtkCallback] private void on_notify_active_name (GLib.Object object, GLib.ParamSpec pspec) { // Detect user selection var timeframe = Ft.Timeframe.from_string (this.timeframe_toggle_group.active_name); if (timeframe != this._timeframe) { this.timeframe_selected (timeframe); } } [GtkCallback] private void on_day_selected (GLib.Date date) { this.user_selected_date = date.copy (); this.date_selected (this.user_selected_date); } [GtkCallback] private void on_week_selected (GLib.Date date) { this.user_selected_date = this.refine_date (date, Ft.Timeframe.WEEK); this.date_selected (this.user_selected_date); } [GtkCallback] private void on_month_selected (GLib.Date date) { this.user_selected_date = this.refine_date (date, Ft.Timeframe.MONTH); this.date_selected (this.user_selected_date); } [GtkCallback] private void on_week_display_changed (GLib.DateMonth display_month, GLib.DateYear display_year) { this.user_selected_date = this.refine_date (this._date, Ft.Timeframe.WEEK, display_month, display_year); this.date_selected (this.user_selected_date); } public override void unmap () { base.unmap (); this.user_selected_date = GLib.Date (); } [Signal (run = "last")] public signal void timeframe_selected (Ft.Timeframe timeframe) { this.timeframe = timeframe; } [Signal (run = "last")] public signal void date_selected (GLib.Date date) { this.date = date; } } } focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/week-chooser.ui000066400000000000000000000047121520625676500265510ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/stats/widgets/week-chooser.vala000066400000000000000000000333361520625676500270630ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/stats/widgets/week-chooser.ui")] public sealed class WeekChooser : Adw.Bin { private const int DAYS_PER_WEEK = 7; public GLib.Date selected_date { get { return this._selected_date; } set { this._selected_date = value.copy (); if (!this._selected_date.valid ()) { this.update_selection (); return; } var display_month = this._selected_date.get_month (); var display_year = this._selected_date.get_year (); if (this._display_month != display_month || this._display_year != display_year) { this.set_display_month_year (display_month, display_year); } else { this.update_selection (); } } } public GLib.Date min_date { get { return this._min_date; } set { this._min_date = value.copy (); this.update_previous_button (); } } public GLib.Date max_date { get { return this._max_date; } set { this._max_date = value.copy (); this.update_next_button (); } } public GLib.DateMonth display_month { get { return this._display_month; } } public GLib.DateYear display_year { get { return this._display_year; } } [GtkChild] private unowned Gtk.Label header_label; [GtkChild] private unowned Gtk.Button previous_button; [GtkChild] private unowned Gtk.Button next_button; [GtkChild] private unowned Gtk.Box weekdays_box; [GtkChild] private unowned Gtk.Grid grid; private GLib.Date _selected_date; private GLib.Date _min_date; private GLib.Date _max_date; private GLib.DateMonth _display_month; private GLib.DateYear _display_year; private unowned Gtk.Widget selected_button = null; private GLib.Date display_start_date; private GLib.Date display_end_date; static construct { set_css_name ("calendar"); } private void set_display_month_year (GLib.DateMonth month, GLib.DateYear year) { if (this._display_month == month && this._display_year == year) { return; } this._display_month = month; this._display_year = year; var display_date = GLib.Date (); display_date.set_dmy (1, month, year); this.display_start_date = Ft.Timeframe.WEEK.normalize_date (display_date); this.display_end_date = display_date.copy (); this.display_end_date.add_months (1U); this.display_end_date = Ft.Timeframe.WEEK.normalize_date (this.display_end_date); if (this.display_end_date.get_month () == display_date.get_month ()) { this.display_end_date.add_days (7U); } this.display_end_date.subtract_days (1U); this.update (); if (this.get_mapped ()) { this.display_changed (this._display_month, this._display_year); } } internal void set_selected_date_full (GLib.Date value, GLib.DateMonth display_month, GLib.DateYear display_year) { this._selected_date = value.copy (); if (!this._selected_date.valid ()) { this.update_selection (); return; } if (this._display_month != display_month || this._display_year != display_year) { this.set_display_month_year (display_month, display_year); } else { this.update_selection (); } } private Gtk.Button create_week_button (GLib.Date date, uint week_number) { var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); box.homogeneous = true; var week_label = new Gtk.Label (@"$(week_number)"); week_label.add_css_class ("week-number"); box.append (week_label); var day_date = date.copy (); for (var day = 1; day <= DAYS_PER_WEEK; day++) { var day_label = new Gtk.Label (@"$(day_date.get_day())"); day_label.add_css_class ("day-number"); if (day_date.get_month () != this._display_month) { day_label.add_css_class ("dim-label"); } box.append (day_label); day_date.add_days (1U); } var button = new Gtk.Button (); button.halign = Gtk.Align.FILL; button.valign = Gtk.Align.FILL; button.height_request = 32; button.child = box; button.add_css_class ("week"); button.add_css_class ("pill"); button.add_css_class ("flat"); unowned var self = this; var date_copy = date.copy (); button.clicked.connect ( () => { self.select (date_copy); }); return button; } private void clear_weekdays () { var child = this.weekdays_box.get_first_child (); while (child != null) { var next_child = child.get_next_sibling (); this.weekdays_box.remove (child); child = next_child; } } private void clear_weeks () { var child = this.grid.get_first_child (); while (child != null) { var next_child = child.get_next_sibling (); this.grid.remove (child); child = next_child; } this.selected_button = null; } /** * We place weekday labels as first row of the grid. */ private void update_weekday_labels () { this.clear_weekdays (); var date = this.display_start_date.copy (); var hash_label = new Gtk.Label (""); hash_label.xalign = 0.5f; hash_label.yalign = 0.5f; this.weekdays_box.append (hash_label); for (var column = 0; column < DAYS_PER_WEEK; column++) { var day_name = Ft.DateUtils.format_date (date, "%a"); var day_letter = day_name.get_char (0).toupper ().to_string (); var label = new Gtk.Label (day_letter); label.xalign = 0.5f; label.yalign = 0.5f; this.weekdays_box.append (label); date.add_days (1U); } } private void update_previous_button () { this.previous_button.sensitive = !this._min_date.valid () || this._display_month > this._min_date.get_month () || this._display_year > this._min_date.get_year (); } private void update_next_button () { this.next_button.sensitive = !this._max_date.valid () || this._display_month < this._max_date.get_month () || this._display_year < this._max_date.get_year (); } private void update_header () { var month_name = capitalize_words ( Ft.DateUtils.get_month_name (this._display_month)); var year = (int) this._display_year; this.header_label.label = @"$(month_name) $(year)"; this.update_previous_button (); this.update_next_button (); } private void create_weeks () { var week_start_date = this.display_start_date.copy (); var week_end_date = week_start_date.copy (); week_end_date.add_days (6U); var year_start_date = GLib.Date (); year_start_date.set_dmy (1, 1, this._display_year); var is_min_date_valid = this._min_date.valid (); var is_max_date_valid = this._max_date.valid (); var first_day_of_week = Ft.Locale.get_first_day_of_week (); var week_number_offset = 1U - ( first_day_of_week == GLib.DateWeekday.MONDAY ? year_start_date.get_monday_week_of_year () : year_start_date.get_sunday_week_of_year () ); var row = 0; while (week_start_date.compare (this.display_end_date) <= 0) { var week_number = ( first_day_of_week == GLib.DateWeekday.MONDAY ? week_end_date.get_monday_week_of_year () : week_end_date.get_sunday_week_of_year () ) + week_number_offset; var button = this.create_week_button (week_start_date, week_number); button.sensitive = (!is_min_date_valid || this._min_date.compare (week_end_date) <= 0) && (!is_max_date_valid || this._max_date.compare (week_start_date) >= 0); this.grid.attach (button, 0, row, 1, 1); week_start_date.add_days (7U); week_end_date.add_days (7U); row++; } } private void update_weeks () { if (!this.get_mapped ()) { return; } this.clear_weeks (); this.create_weeks (); } private void update_selection () { if (this.selected_button != null) { this.selected_button.unset_state_flags (Gtk.StateFlags.SELECTED); this.selected_button = null; } if (this.display_start_date.valid () && this._selected_date.valid ()) { var position = this.display_start_date.days_between (this._selected_date); var row = position / DAYS_PER_WEEK; this.selected_button = this.grid.get_child_at (0, row); this.selected_button?.set_state_flags (Gtk.StateFlags.SELECTED, false); } } private void update () { this.update_header (); this.update_weekday_labels (); this.update_weeks (); this.update_selection (); } [GtkCallback] private void on_previous_button_clicked () { var month = this._display_month; var year = this._display_year; if (month == GLib.DateMonth.JANUARY) { year--; month = GLib.DateMonth.DECEMBER; } else { month = (GLib.DateMonth)(Ft.DateUtils.get_month_number (month) - 1U); } this.set_display_month_year (month, year); } [GtkCallback] private void on_next_button_clicked () { var month = this._display_month; var year = this._display_year; if (month == GLib.DateMonth.DECEMBER) { year++; month = GLib.DateMonth.JANUARY; } else { month = (GLib.DateMonth)(Ft.DateUtils.get_month_number (month) + 1U); } this.set_display_month_year (month, year); } private bool select (GLib.Date date) { if (!date.valid ()) { return false; } if (this._selected_date.valid () && this._selected_date.compare (date) == 0) { return true; } this.selected_date = date; this.selected (this._selected_date); return true; } public void reset () { if (this._selected_date.valid ()) { this.set_display_month_year (this._selected_date.get_month (), this._selected_date.get_year ()); } else { var today = Ft.DateUtils.get_today (); this.set_display_month_year (today.get_month (), today.get_year ()); } } public override void map () { this.reset (); this.create_weeks (); this.update_selection (); base.map (); } public override void unmap () { base.unmap (); // HACK: CSS animations kick in when widgets gets mapped, // to avoid them just remove children this.clear_weeks (); } public signal void selected (GLib.Date date); public signal void display_changed (GLib.DateMonth display_month, GLib.DateYear display_year); public override void dispose () { this.selected_button = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/000077500000000000000000000000001520625676500221275ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/timer/compact-timer-view.ui000066400000000000000000000112331520625676500262020ustar00rootroot00000000000000
_Pomodoro session-manager.state pomodoro _Short Break session-manager.state short-break _Long Break session-manager.state long-break
focustimerhq-FocusTimer-8581be2/src/ui/main/timer/compact-timer-view.vala000066400000000000000000000142731520625676500265170ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/timer/compact-timer-view.ui")] public class CompactTimerView : Adw.Bin, Gtk.Buildable { [GtkChild] private unowned Gtk.MenuButton state_menubutton; [GtkChild] private unowned Ft.TimerLabel timer_label; private Ft.SessionManager session_manager; private Ft.Timer timer; private GLib.MenuModel? state_menu; private GLib.MenuModel? uniform_state_menu; private ulong timer_state_changed_id = 0; private ulong notify_current_time_block_id = 0; private ulong notify_has_uniform_breaks_id = 0; private ulong current_time_block_changed_id = 0; private Ft.TimeBlock? current_time_block; static construct { set_css_name ("compacttimerview"); } construct { this.session_manager = Ft.SessionManager.get_default (); this.timer = session_manager.timer; var builder = new Gtk.Builder.from_resource ("/io/github/focustimerhq/FocusTimer/ui/main/timer/menus.ui"); this.state_menu = (GLib.MenuModel) builder.get_object ("state_menu"); this.uniform_state_menu = (GLib.MenuModel) builder.get_object ("uniform_state_menu"); this.notify_current_time_block_id = this.session_manager.notify["current-time-block"].connect ( this.on_session_manager_notify_current_time_block); this.notify_has_uniform_breaks_id = this.session_manager.notify["has-uniform-breaks"].connect ( this.on_session_manager_notify_has_uniform_breaks); this.on_session_manager_notify_current_time_block (); this.on_session_manager_notify_has_uniform_breaks (); } private string get_state_label () { var current_time_block = this.session_manager.current_time_block; var current_state = current_time_block != null ? current_time_block.state : Ft.State.STOPPED; return current_state.get_label (); } private void update_css_classes () { if (this.timer.is_running ()) { this.state_menubutton.add_css_class ("timer-running"); } else { this.state_menubutton.remove_css_class ("timer-running"); } } private void update_buttons () { this.state_menubutton.label = !this.timer.is_finished () ? this.get_state_label () : _("Finished!"); } private void update_timer_label_placeholder () { var session_template = this.session_manager.scheduler.session_template; this.timer_label.placeholder_has_hours = session_template.pomodoro_duration >= Ft.Interval.HOUR; } private void update () { this.update_css_classes (); this.update_buttons (); this.update_timer_label_placeholder (); } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update (); } private void on_current_time_block_changed () { this.update_buttons (); } private void on_session_manager_notify_current_time_block () { var current_time_block = this.session_manager.current_time_block; if (this.current_time_block_changed_id != 0) { this.current_time_block.disconnect (this.current_time_block_changed_id); this.current_time_block_changed_id = 0; } if (current_time_block != null) { this.current_time_block_changed_id = current_time_block.changed.connect (this.on_current_time_block_changed); } this.current_time_block = current_time_block; this.on_current_time_block_changed (); } private void on_session_manager_notify_has_uniform_breaks () { this.state_menubutton.menu_model = this.session_manager.has_uniform_breaks ? this.uniform_state_menu : this.state_menu; } private void connect_signals () { if (this.timer_state_changed_id == 0) { this.timer_state_changed_id = timer.state_changed.connect (this.on_timer_state_changed); } } private void disconnect_signals () { if (this.timer_state_changed_id != 0) { this.timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } } public override void map () { this.session_manager.ensure_session (); this.update (); base.map (); this.connect_signals (); } public override void unmap () { base.unmap (); this.disconnect_signals (); } public override void dispose () { this.disconnect_signals (); if (this.current_time_block_changed_id != 0) { this.current_time_block.disconnect (this.current_time_block_changed_id); this.current_time_block_changed_id = 0; } if (this.notify_current_time_block_id != 0) { this.session_manager.disconnect (this.notify_current_time_block_id); this.notify_current_time_block_id = 0; } if (this.notify_has_uniform_breaks_id != 0) { this.session_manager.disconnect (this.notify_has_uniform_breaks_id); this.notify_has_uniform_breaks_id = 0; } this.state_menu = null; this.uniform_state_menu = null; this.current_time_block = null; this.session_manager = null; this.timer = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/menus.ui000066400000000000000000000020741520625676500236200ustar00rootroot00000000000000
_Pomodoro session-manager.start-pomodoro _Short Break session-manager.start-short-break _Long Break session-manager.start-long-break
_Pomodoro session-manager.start-pomodoro _Break session-manager.start-break
focustimerhq-FocusTimer-8581be2/src/ui/main/timer/timer-view.ui000066400000000000000000000062361520625676500245650ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/timer/timer-view.vala000066400000000000000000000617551520625676500251020ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/timer/timer-view.ui")] public class TimerView : Gtk.Widget, Gtk.Buildable { /** * MIN_PADDING and MAX_PADDING are applied to left and right sides and scale according to width. * * BOTTOM_PADDING aims to counter padding used in state_menubutton. */ private int MIN_PADDING = 24; private int MAX_PADDING = 72; private int BOTTOM_PADDING = 5; /** * Size of the outer bounds, not including padding. */ private int MIN_WIDTH = 300; private int NAT_WIDTH = 400; private int MAX_WIDTH = 450; /** * Vertical spacing between children. */ private int MIN_SPACING = 12; private int NAT_SPACING = 24; private int MAX_SPACING = 50; /** * Relative width of the timer label and session indicator. */ private float INNER_RELATIVE_WIDTH = 0.70f; /** * Relative height limit of the progress ring. */ private float MAX_PROGRESSRING_RELATIVE_HEIGHT = 0.618f; [GtkChild] private unowned Gtk.MenuButton state_menubutton; [GtkChild] private unowned Gtk.Button open_screen_overlay_button; [GtkChild] private unowned Gtk.Box header_box; [GtkChild] private unowned Gtk.Box inner_box; [GtkChild] private unowned Ft.TimerProgressBar timer_progressbar; [GtkChild] private unowned Ft.SessionProgressBar session_progressbar; [GtkChild] private unowned Ft.TimerLabel timer_label; [GtkChild] private unowned Ft.TimerControlButtons timer_control_buttons; [GtkChild] private unowned Gtk.GestureClick click_gesture; [GtkChild] private unowned Gtk.GestureDrag drag_gesture; private Ft.SessionManager session_manager; private Ft.Timer timer; private GLib.Settings? settings = null; private GLib.MenuModel? state_menu; private GLib.MenuModel? uniform_state_menu; private ulong timer_state_changed_id = 0; private ulong session_expired_id = 0; private ulong notify_current_time_block_id = 0; private ulong notify_has_uniform_breaks_id = 0; private ulong current_time_block_changed_id = 0; private ulong settings_changed_id = 0; private Adw.Toast? session_expired_toast; private Ft.TimeBlock? current_time_block; static construct { set_css_name ("timerview"); // TODO: move these keybindings to window // currently they work only if view is in focus add_binding_action (Gdk.Key.minus, Gdk.ModifierType.CONTROL_MASK, "timer.shorten", null); add_binding_action (Gdk.Key.equal, Gdk.ModifierType.CONTROL_MASK, "timer.extend", null); } construct { this.session_manager = Ft.SessionManager.get_default (); this.timer = session_manager.timer; this.settings = Ft.get_settings (); var builder = new Gtk.Builder.from_resource ("/io/github/focustimerhq/FocusTimer/ui/main/timer/menus.ui"); this.state_menu = (GLib.MenuModel) builder.get_object ("state_menu"); this.uniform_state_menu = (GLib.MenuModel) builder.get_object ("uniform_state_menu"); this.session_manager.bind_property ("current-session", this.session_progressbar, "session", GLib.BindingFlags.SYNC_CREATE); this.settings_changed_id = this.settings.changed.connect (this.on_settings_changed); this.session_expired_id = this.session_manager.session_expired.connect (this.on_session_expired); this.notify_current_time_block_id = this.session_manager.notify["current-time-block"].connect ( this.on_session_manager_notify_current_time_block); this.notify_has_uniform_breaks_id = this.session_manager.notify["has-uniform-breaks"].connect ( this.on_session_manager_notify_has_uniform_breaks); this.on_session_manager_notify_current_time_block (); this.on_session_manager_notify_has_uniform_breaks (); } private string get_state_label () { var current_time_block = this.session_manager.current_time_block; var current_state = current_time_block != null ? current_time_block.state : Ft.State.STOPPED; return current_state.get_label (); } private void update_buttons () { var current_state = this.current_time_block != null ? this.current_time_block.state : Ft.State.STOPPED; this.state_menubutton.label = !this.timer.is_finished () ? this.get_state_label () : _("Finished!"); this.open_screen_overlay_button.visible = this.settings.get_boolean ("screen-overlay") && current_state.is_break () && !this.timer.is_finished (); } private void update_timer_label_placeholder () { var session_template = this.session_manager.scheduler.session_template; this.timer_label.placeholder_has_hours = session_template.pomodoro_duration >= Ft.Interval.HOUR; } private void update () { this.update_buttons (); this.update_timer_label_placeholder (); if (this.session_expired_toast != null && this.timer.is_running ()) { this.session_expired_toast.dismiss (); } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update (); } [GtkCallback] private void on_pressed (Gtk.GestureClick gesture, int n_press, double x, double y) { var sequence = gesture.get_current_sequence (); var event = gesture.get_last_event (sequence); if (event == null) { return; } if (n_press > 1) { this.drag_gesture.set_state (Gtk.EventSequenceState.DENIED); } } [GtkCallback] private void on_drag_update (Gtk.GestureDrag gesture, double offset_x, double offset_y) { double start_x, start_y; double native_x, native_y; if (!Gtk.drag_check_threshold (this, 0, 0, (int) offset_x, (int) offset_y)) { return; } gesture.set_state (Gtk.EventSequenceState.CLAIMED); gesture.get_start_point (out start_x, out start_y); var widget_point = Graphene.Point (); widget_point.init ((float) start_x, (float) start_x); var native_point = Graphene.Point (); var native = this.get_native (); var toplevel = native.get_surface () as Gdk.Toplevel; if (toplevel != null && gesture.widget.compute_point (native, widget_point, out native_point)) { native.get_surface_transform (out native_x, out native_y); toplevel.begin_move ( gesture.get_device (), (int) gesture.get_current_button (), native_x + (double) native_point.x, native_y + (double) native_point.y, gesture.get_current_event_time ()); } this.drag_gesture.reset (); this.click_gesture.reset (); } /** * We want to notify that the app stopped the timer because session has expired. * But, its not worth users attention in cases where resetting a session is to be expected. */ private void on_session_expired (Ft.Session session) { var timestamp = Ft.Timestamp.from_now (); // Skip the toast if the timer was stopped and no pomodoro has been completed. var completed_time_blocks_count = session.count_time_blocks ( (time_block) => { return time_block.state == Ft.State.POMODORO && time_block.get_status () == Ft.TimeBlockStatus.COMPLETED; }); if (!this.timer.is_started () && completed_time_blocks_count == 0U) { return; } // Skip the toast if session expired more than 4 hours ago. if (timestamp - session.expiry_time >= 4 * Ft.Interval.HOUR) { return; } var window = this.get_root () as Ft.Window; assert (window != null); var toast = new Adw.Toast (_("Session has expired")); toast.use_markup = false; toast.priority = Adw.ToastPriority.HIGH; toast.dismissed.connect (() => { this.session_expired_toast = null; }); this.session_expired_toast = toast; window.add_toast (toast); } private void on_current_time_block_changed () { this.update_buttons (); } private void on_session_manager_notify_current_time_block () { var current_time_block = this.session_manager.current_time_block; if (this.current_time_block_changed_id != 0) { this.current_time_block.disconnect (this.current_time_block_changed_id); this.current_time_block_changed_id = 0; } if (current_time_block != null) { this.current_time_block_changed_id = current_time_block.changed.connect ( this.on_current_time_block_changed); } this.current_time_block = current_time_block; this.on_current_time_block_changed (); } private void on_session_manager_notify_has_uniform_breaks () { this.state_menubutton.menu_model = this.session_manager.has_uniform_breaks ? this.uniform_state_menu : this.state_menu; } private void on_settings_changed (GLib.Settings settings, string key) { switch (key) { case "screen-overlay": this.update_buttons (); break; } } private void connect_signals () { if (this.timer_state_changed_id == 0) { this.timer_state_changed_id = timer.state_changed.connect (this.on_timer_state_changed); } } private void disconnect_signals () { if (this.timer_state_changed_id != 0) { this.timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } } private void calculate_height_for_width (int avaliable_width, out int minimum_height, out int natural_height) { var tmp_minimum_height = 0; var tmp_natural_height = 0; minimum_height = 2 * MIN_SPACING; natural_height = 2 * NAT_SPACING; this.header_box.measure (Gtk.Orientation.VERTICAL, avaliable_width, out tmp_minimum_height, out tmp_natural_height, null, null); minimum_height += tmp_minimum_height; natural_height += tmp_natural_height; this.timer_progressbar.measure (Gtk.Orientation.VERTICAL, avaliable_width, out tmp_minimum_height, out tmp_natural_height, null, null); minimum_height += tmp_minimum_height; natural_height += tmp_natural_height; this.timer_control_buttons.measure (Gtk.Orientation.VERTICAL, avaliable_width, out tmp_minimum_height, out tmp_natural_height, null, null); minimum_height += tmp_minimum_height; natural_height += tmp_natural_height; } private void calculate_width_for_height (int avaliable_height, out int minimum_width, out int natural_width) { var tmp_minimum_width = 0; var tmp_natural_width = 0; var tmp_natural_height = 0; minimum_width = MIN_WIDTH; natural_width = MIN_WIDTH; avaliable_height -= 2 * NAT_SPACING; this.header_box.measure (Gtk.Orientation.HORIZONTAL, -1, out tmp_minimum_width, out tmp_natural_width, null, null); minimum_width = int.max (minimum_width, tmp_minimum_width); natural_width = int.max (natural_width, tmp_natural_width); this.header_box.measure (Gtk.Orientation.VERTICAL, natural_width, null, out tmp_natural_height, null, null); avaliable_height -= tmp_natural_height; this.timer_control_buttons.measure (Gtk.Orientation.VERTICAL, natural_width, null, out tmp_natural_height, null, null); avaliable_height -= tmp_natural_height; this.timer_progressbar.measure (Gtk.Orientation.HORIZONTAL, avaliable_height, null, out tmp_natural_width, null, null); natural_width = int.max (natural_width, tmp_natural_width); if (natural_width > MAX_WIDTH) { natural_width = MAX_WIDTH; } } private int calculate_padding (int width) { var min_padding = (float) MIN_PADDING; var max_padding = (float) MAX_PADDING; var t = ((float) (width - MIN_WIDTH) / (float) MAX_WIDTH).clamp (0.0f, 1.0f); return (int) Math.roundf ((1.0f - t) * min_padding + t * max_padding); } private int calculate_padding_inv (int padded_width) { var min_padded_width = (float) (MIN_WIDTH + 2 * MIN_PADDING); var max_padded_width = (float) (MAX_WIDTH + 2 * MAX_PADDING); var t = (padded_width - min_padded_width) / (max_padded_width - min_padded_width); return (int) Math.roundf ( 2.0f * ((1.0f - t) * (float) MIN_WIDTH + t * (float) MAX_WIDTH)) / 2; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.WIDTH_FOR_HEIGHT; } /** * Define two main guides: outer and inner. Outer is intended as a * container for major widgets. Inner is intended for the timer * label and session progress bar. * * The constraints try to fit height of the outer guide to the window. * Than its constrained by max width. */ public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var minimum_padding = MIN_PADDING; var natural_padding = MIN_PADDING; minimum = MIN_WIDTH; natural = NAT_WIDTH; if (orientation == Gtk.Orientation.HORIZONTAL) { if (for_size != -1) { this.calculate_width_for_height (for_size, out minimum, out natural); } natural_padding = this.calculate_padding (natural); } else { this.calculate_height_for_width (MIN_WIDTH, out minimum, out minimum); if (for_size != -1) { this.calculate_height_for_width (for_size.clamp (MIN_WIDTH, MAX_WIDTH), null, out natural); } } if (natural < minimum) { natural = minimum; } minimum += minimum_padding * 2; natural += natural_padding * 2; minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { // Determine width of the outer bounds. var allocation = Gtk.Allocation () { width = this.calculate_padding_inv (width).clamp (MIN_WIDTH, MAX_WIDTH), height = height - BOTTOM_PADDING }; allocation.width = int.min ( allocation.width, (int) Math.floorf (MAX_PROGRESSRING_RELATIVE_HEIGHT * (float) height)); var tmp_natural_height = 0; var tmp_minimum_width = 0; var tmp_natural_width = 0; this.calculate_height_for_width (allocation.width, null, out tmp_natural_height); if (tmp_natural_height > allocation.height) { this.calculate_width_for_height (allocation.height, null, out allocation.width); } // Determine header_box size. var header_box_allocation = Gtk.Allocation () { width = allocation.width }; this.header_box.measure ( Gtk.Orientation.VERTICAL, header_box_allocation.width, null, out header_box_allocation.height, null, null); // Determine timer_progressbar size. var timer_progressbar_allocation = Gtk.Allocation () { width = allocation.width, height = allocation.width }; // Determine inner_box size. var inner_box_allocation = Gtk.Allocation () { width = (int) Math.roundf ( timer_progressbar_allocation.width * INNER_RELATIVE_WIDTH / 2.0f) * 2, height = 0 }; this.inner_box.measure ( Gtk.Orientation.VERTICAL, inner_box_allocation.width, null, out inner_box_allocation.height, null, null); // Determine timer_control_buttons size. var timer_control_buttons_allocation = Gtk.Allocation (); this.timer_control_buttons.measure ( Gtk.Orientation.HORIZONTAL, -1, out tmp_minimum_width, out tmp_natural_width, null, null); timer_control_buttons_allocation.width = int.max( allocation.width.clamp (tmp_minimum_width, tmp_natural_width), inner_box_allocation.width + 60); this.timer_control_buttons.measure ( Gtk.Orientation.VERTICAL, timer_control_buttons_allocation.width, null, out timer_control_buttons_allocation.height, null, null); // Position children horizontally. allocation.x = (width - allocation.width) / 2; header_box_allocation.x = allocation.x; timer_progressbar_allocation.x = allocation.x; inner_box_allocation.x = (width - inner_box_allocation.width) / 2; timer_control_buttons_allocation.x = (width - timer_control_buttons_allocation.width) / 2; // Position children vertically. tmp_natural_height = header_box_allocation.height + timer_progressbar_allocation.height + timer_control_buttons_allocation.height; var spacing = ((height - tmp_natural_height) / 4).clamp (MIN_SPACING, MAX_SPACING); allocation.y = (height - tmp_natural_height - 2 * spacing) / 2 - BOTTOM_PADDING; header_box_allocation.y = allocation.y; timer_progressbar_allocation.y = header_box_allocation.y + header_box_allocation.height + spacing; inner_box_allocation.y = timer_progressbar_allocation.y + (timer_progressbar_allocation.height - inner_box_allocation.height) / 2; timer_control_buttons_allocation.y = timer_progressbar_allocation.y + timer_progressbar_allocation.height + spacing; this.header_box.allocate_size (header_box_allocation, -1); this.timer_progressbar.allocate_size (timer_progressbar_allocation, -1); this.inner_box.allocate_size (inner_box_allocation, -1); this.timer_control_buttons.allocate_size (timer_control_buttons_allocation, -1); } public override void map () { this.session_manager.ensure_session (); this.update (); base.map (); this.connect_signals (); } public override void unmap () { base.unmap (); this.disconnect_signals (); } public override void dispose () { this.disconnect_signals (); if (this.session_expired_id != 0) { this.session_manager.disconnect (this.session_expired_id); this.session_expired_id = 0; } if (this.current_time_block_changed_id != 0) { this.current_time_block.disconnect (this.current_time_block_changed_id); this.current_time_block_changed_id = 0; } if (this.notify_current_time_block_id != 0) { this.session_manager.disconnect (this.notify_current_time_block_id); this.notify_current_time_block_id = 0; } if (this.notify_has_uniform_breaks_id != 0) { this.session_manager.disconnect (this.notify_has_uniform_breaks_id); this.notify_has_uniform_breaks_id = 0; } if (this.settings_changed_id != 0) { this.settings.disconnect (this.settings_changed_id); this.settings_changed_id = 0; } this.session_expired_toast = null; this.state_menu = null; this.uniform_state_menu = null; this.current_time_block = null; this.session_manager = null; this.timer = null; this.settings = null; // HACK: Without this children do not get disposed properly this.dispose_template (typeof (Ft.TimerView)); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/000077500000000000000000000000001520625676500235755ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/session-progress-bar.vala000066400000000000000000001466301520625676500305430ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public sealed class SessionProgressBar : Gtk.Widget { private const float DEFAULT_LINE_WIDTH = 6.0f; private const float SEGMENT_SPACING = 0.17f; private const int MIN_WIDTH = 100; private const uint TIMEOUT_RESOLUTION = 3U; private const uint MIN_TIMEOUT_INTERVAL = 25; // 40Hz private const uint FADE_IN_DURATION = 500; private const uint FADE_OUT_DURATION = 500; private const uint VALUE_ANIMATION_DURATION = 500; private const uint SCALE_ANIMATION_DURATION = 700; sealed class Segment : Gtk.Widget { public Ft.Timer timer { get { return this._timer; } construct { this._timer = value; } } public Ft.Cycle cycle { get { return this._cycle; } set { if (this._cycle == value) { return; } if (this.cycle_changed_id != 0) { this._cycle.disconnect (this.cycle_changed_id); this.cycle_changed_id = 0; } this._cycle = value; if (this._cycle != null) { this.cycle_changed_id = this._cycle.changed.connect (this.on_cycle_changed); } this.on_cycle_changed (); this.queue_draw_all (); } } public float span_start { get { return this._span_start; } } public float span_end { get { return this._span_end; } } public float weight { get { return this._weight; } set { this._weight = value; } } [CCode (notify = false)] public float line_width { get { return this._line_width; } set { if (this._line_width != value) { this._line_width = value; this.notify_property ("line-width"); this.queue_draw_all (); } } } public float display_value { get { return this._display_value; } } private Ft.Cycle? _cycle = null; private Ft.Timer? _timer = null; private float _line_width; private float _span_start = 0.0f; private float _span_end = 0.0f; private float _weight = 0.0f; private float _display_value = 0.0f; private ulong cycle_changed_id = 0U; private ulong tick_id = 0U; private uint tick_callback_id = 0U; private uint timeout_id = 0U; private uint timeout_interval = 0U; private unowned Ft.Gizmo through = null; private unowned Ft.Gizmo highlight = null; private Graphene.Rect bounds; private Gsk.RoundedRect outline; private float value_animation_progress = 1.0f; private Adw.TimedAnimation? weight_animation = null; internal float display_value_from = 0.0f; internal float display_value_to = 0.0f; construct { var through = new Ft.Gizmo ( Segment.measure_child_cb, null, Segment.snapshot_through_cb, null, null, null); through.focusable = false; through.add_css_class ("through"); through.set_parent (this); var highlight = new Ft.Gizmo ( Segment.measure_child_cb, null, Segment.snapshot_highlight_cb, null, null, null); highlight.focusable = false; highlight.add_css_class ("highlight"); highlight.insert_after (this, through); this.highlight = highlight; this.through = through; } public Segment (Ft.Cycle cycle) { GLib.Object ( timer: Ft.Timer.get_default (), cycle: cycle ); } private inline int64 get_current_time () { return this._timer.is_running () ? this._timer.get_current_time (this.get_frame_clock ().get_frame_time ()) : this._timer.get_last_state_changed_time (); } internal void prepare_value_animation (float display_value_from, float display_value_to) { this.display_value_from = display_value_from; this.display_value_to = display_value_to; this.value_animation_progress = this._display_value == display_value_to ? 1.0f : 0.0f; } internal void finish_value_animation () { if (this.value_animation_progress != 1.0f) { this.value_animation_progress = 1.0f; this.highlight.queue_draw (); } } internal void set_value_animation_progress (float progress) { if (this.value_animation_progress != progress) { this.value_animation_progress = progress; this.highlight.queue_draw (); } } internal void animate_weight (float weight_from, float weight_to, owned Adw.CallbackAnimationTarget target) { if (weight_to == weight_from) { return; } if (this.weight_animation != null) { this.weight_animation.pause (); this.weight_animation = null; } this.weight_animation = new Adw.TimedAnimation (this, (double) weight_from, (double) weight_to, SCALE_ANIMATION_DURATION, target); this.weight_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.weight_animation.done.connect ( () => { this.weight_animation = null; }); this.weight_animation.play (); } private inline void queue_draw_all () { this.queue_draw (); this.through.queue_draw (); this.highlight.queue_draw (); } private static void measure_child_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { // `SessionProgressBar` dictates the size. Gizmos fill available space. minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } private static void snapshot_through_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = (Segment) gizmo.parent; if (self != null) { self.snapshot_through (gizmo, snapshot); } } private static void snapshot_highlight_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = (Segment) gizmo.parent; if (self != null) { self.snapshot_highlight (gizmo, snapshot); } } private void snapshot_through (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { snapshot.push_rounded_clip (this.outline); snapshot.append_color (gizmo.get_color (), this.bounds); snapshot.pop (); } private void snapshot_highlight (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var timestamp = this.get_current_time (); var display_value = this._cycle != null ? (float) this._cycle.calculate_progress (timestamp) : 0.0f; var color = gizmo.get_color (); if (this.value_animation_progress < 1.0f) { display_value = lerpf (this.display_value_from, display_value, this.value_animation_progress); } if (display_value > 0.0f && display_value < 1.0f) { var width = (float) this.get_width (); var height = (float) this.get_height (); var highlight_width = (this._span_end - this._span_start) * display_value * width; var highlight_height = this._line_width; var highlight_x = this._span_start * width; var highlight_y = (height - highlight_height) / 2.0f; var highlight_bounds = Graphene.Rect (); var highlight_outline = Gsk.RoundedRect (); var clip_applied = false; if (highlight_width < highlight_height) { highlight_x -= highlight_height - highlight_width; highlight_width = highlight_height; snapshot.push_rounded_clip (this.outline); clip_applied = true; } if (this.get_direction () == Gtk.TextDirection.RTL) { highlight_x = width - highlight_x - highlight_width; } highlight_bounds.init (highlight_x, highlight_y, highlight_width, highlight_height); highlight_outline.init_from_rect (highlight_bounds, highlight_height / 2.0f); snapshot.push_rounded_clip (highlight_outline); snapshot.append_color (color, highlight_bounds); snapshot.pop (); if (clip_applied) { snapshot.pop (); } } else if (display_value == 1.0f) { snapshot.push_rounded_clip (this.outline); snapshot.append_color (color, this.bounds); snapshot.pop (); } this._display_value = display_value; } public void set_span_range (float span_start, float span_end) { var changed = false; if (span_end < span_start) { var tmp = span_start; span_start = span_end; span_end = tmp; } if (this._span_start != span_start) { this._span_start = span_start; changed = true; } if (this._span_end != span_end) { this._span_end = span_end; changed = true; } if (changed) { this.queue_draw_all (); } } private uint calculate_timeout_interval () requires (this._cycle != null) { var timestamp = this.get_current_time (); var distance = (float) (this._span_end - this._span_start) * (float) this.get_width (); var duration = (float) this._cycle.calculate_progress_duration (timestamp); distance *= (float) TIMEOUT_RESOLUTION; return distance > 0.0 ? Ft.Timestamp.to_milliseconds_uint ((int64) Math.roundf (duration / distance)) : 0; } private void start_timeout () requires (this._cycle != null) requires (this.get_mapped ()) { var timeout_interval = this.calculate_timeout_interval (); if (timeout_interval < MIN_TIMEOUT_INTERVAL) { timeout_interval = 0U; } if (this.timeout_interval != timeout_interval) { this.timeout_interval = timeout_interval; this.stop_timeout (); } if (this.tick_id == 0 && timeout_interval > 0 && timeout_interval > 500) { this.tick_id = this._timer.tick.connect ( (timestamp) => { this.highlight.queue_draw (); }); } else if (this.timeout_id == 0 && timeout_interval > 0) { this.timeout_id = GLib.Timeout.add ( timeout_interval, () => { this.highlight.queue_draw (); return GLib.Source.CONTINUE; }); GLib.Source.set_name_by_id (this.timeout_id, "Ft.SessionProgressBar.Segment.queue_draw"); } else if (this.tick_callback_id == 0 && timeout_interval == 0) { this.tick_callback_id = this.add_tick_callback ( () => { this.highlight.queue_draw (); return GLib.Source.CONTINUE; }); } } private void stop_timeout () { if (this.tick_id != 0) { this._timer.disconnect (this.tick_id); this.tick_id = 0; } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } if (this.tick_callback_id != 0) { this.remove_tick_callback (this.tick_callback_id); this.tick_callback_id = 0; } } internal bool has_timeout () { return this.tick_id != 0 || this.timeout_id != 0 || this.tick_callback_id != 0; } internal void update_timeout () { this.stop_timeout (); if (!this.get_mapped () || this._cycle == null) { return; } var current_time_block = this._timer.user_data as Ft.TimeBlock; if (current_time_block != null && !this._cycle.contains (current_time_block)) { return; } if (this._timer.is_running ()) { this.start_timeout (); } } public void update () { this.update_timeout (); } private void on_cycle_changed () { var weight = this._cycle != null ? (float) this._cycle.get_weight () : 0.0f; if (weight <= 0.0f) { return; // retain last weight } // Set the initial weight. Let patent animate weights. if (this._weight != weight && this._weight == 0.0f) { this._weight = weight; } this.update (); } public override void map () { base.map (); this.update_timeout (); } public override void unmap () { this.stop_timeout (); base.unmap (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = orientation == Gtk.Orientation.HORIZONTAL ? MIN_WIDTH : (int) Math.ceilf (this._line_width); natural = minimum; minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { this.through.allocate (width, height, baseline, null); this.highlight.allocate (width, height, baseline, null); } public override void snapshot (Gtk.Snapshot snapshot) { var width = (float) this.get_width (); var height = (float) this.get_height (); var segment_x = float.max (this._span_start, 0.0f) * width; var segment_y = (height - this._line_width) / 2.0f; var segment_width = float.min (this._span_end, 1.0f) * width - segment_x; var segment_height = this._line_width; if (segment_width <= 0.0f) { return; } if (this.get_direction () == Gtk.TextDirection.RTL) { segment_x = width - segment_x - segment_width; } this.bounds = Graphene.Rect (); this.bounds.init (segment_x, segment_y, segment_width, segment_height); this.outline = Gsk.RoundedRect (); this.outline.init_from_rect (bounds, 0.5f * segment_height); this.snapshot_child (this.through, snapshot); this.snapshot_child (this.highlight, snapshot); } public override void dispose () { this.stop_timeout (); if (this.weight_animation != null) { this.weight_animation.pause (); this.weight_animation = null; } this.through.unparent (); this.highlight.unparent (); this.through = null; this.highlight = null; this._timer = null; this._cycle = null; base.dispose (); } } public Ft.Timer timer { get { return this._timer; } construct { this._timer = value != null ? value : Ft.Timer.get_default (); } } public Ft.SessionManager session_manager { get { return this._session_manager; } construct { this._session_manager = value != null ? value : Ft.SessionManager.get_default (); } } [CCode (notify = false)] public Ft.Session session { get { return this._session; } set { if (this._session == value) { return; } if (this._session != null) { this._session.changed.disconnect (this.on_session_changed); } this._session = value; this.update (); if (this._session != null) { this._session.changed.connect (this.on_session_changed); } this.notify_property ("session"); } } [CCode (notify = false)] public float line_width { get { return this._line_width; } set { if (this._line_width == value) { return; } this._line_width = value; this.notify_property ("line-width"); this.queue_resize (); } } public bool reveal { get { return this._reveal; } } private Ft.Timer? _timer = null; private Ft.SessionManager? _session_manager = null; private Ft.Session? _session = null; private float _line_width = DEFAULT_LINE_WIDTH; private bool _reveal = true; private bool revealing = false; private unowned Segment? current_segment = null; private float scale = float.NAN; private Adw.TimedAnimation? scale_animation = null; private Adw.TimedAnimation? opacity_animation = null; private Adw.TimedAnimation? value_animation = null; private int64 long_break_time = Ft.Timestamp.UNDEFINED; private uint tick_callback_id = 0; static construct { set_css_name ("sessionprogressbar"); } construct { this.has_tooltip = true; } private void remove_segments () { unowned Gtk.Widget? child; while ((child = this.get_first_child ()) != null) { child.unparent (); } this.current_segment = null; this.scale = float.NAN; } /** * Remove segments that are not in the view. */ private void remove_invisible_segments () requires (this.scale_animation == null) { unowned var segment = (Segment?) this.get_first_child (); while (segment != null) { unowned var next_segment = (Segment?) segment.get_next_sibling (); if (segment.span_start >= 1.0f && segment.cycle == null) { segment.unparent (); } segment = next_segment; } } private inline void snapshot_segments (Gtk.Snapshot snapshot) { unowned var child = this.get_first_child (); while (child != null) { this.snapshot_child (child, snapshot); child = child.get_next_sibling (); } } private float calculate_scale (double total_weight, uint cycles_count) { var norm = total_weight + (cycles_count - 1) * (double) SEGMENT_SPACING; return norm > 0.0 ? (float)(1.0 / norm) : 0.0f; } private float get_current_scale () { return this.scale_animation != null ? (float) this.scale_animation.value : this.scale; } private float get_target_scale () { unowned var segment = (Segment?) this.get_first_child (); var cycles_count = 0; var total_weight = 0.0; while (segment != null) { unowned var cycle = segment.cycle; if (cycle != null) { total_weight += cycle.get_weight (); cycles_count++; } segment = (Segment?) segment.get_next_sibling (); } return this.calculate_scale (total_weight, cycles_count); } private unowned Segment? get_current_segment () { unowned var segment = (Segment?) this.get_last_child (); while (segment != null) { if (segment.display_value > 0.0f) { break; } segment = (Segment?) segment.get_prev_sibling (); } return segment; } private float get_current_position () { unowned var segment = (Segment?) this.get_first_child (); var position = 0.0f; while (segment != null) { var display_value = segment.display_value; if (display_value < 1.0f) { position = lerpf (segment.span_start, segment.span_end, display_value); break; } position = segment.span_end; segment = (Segment?) segment.get_next_sibling (); } return position; } private float get_target_position () { unowned var segment = (Segment?) this.get_first_child (); var position = 0.0f; var timestamp = this._timer.is_running () ? this._timer.get_current_time () : this._timer.get_last_state_changed_time (); while (segment != null) { var segment_progress = segment.cycle != null ? (float) segment.cycle.calculate_progress (timestamp) : 0.0f; if (segment_progress <= 0.0f) { break; } if (segment_progress < 1.0f) { position = lerpf (segment.span_start, segment.span_end, segment_progress); break; } position = segment.span_end; segment = (Segment?) segment.get_next_sibling (); } return position; } private void update_segments_span () { var scale = this.scale_animation != null ? (float) this.scale_animation.value : this.scale; var position = 0.0f; var spacing = (float)(SEGMENT_SPACING * scale); unowned var segment = (Segment?) this.get_first_child (); while (segment != null) { segment.set_span_range (position, position + segment.weight * scale); position = segment.span_end + spacing; segment = (Segment?) segment.get_next_sibling (); } this.queue_draw (); } /** * Synchronise segments according to cycles. */ private void update_segments () { if (this._session == null) { this._session_manager.ensure_session (); this._session = this._session_manager.current_session; this.notify_property ("session"); } var cycles = this._session.get_cycles (); var cycles_count = 0U; unowned GLib.List link = cycles.first (); unowned var segment = (Segment?) this.get_first_child (); unowned var current_cycle = this._session_manager.get_current_cycle (); unowned Segment? current_segment = null; while (link != null) { var cycle = link.data; if (link.data.is_visible ()) { if (segment != null) { segment.cycle = cycle; } else { var new_segment = new Segment (cycle); this.bind_property ("line-width", new_segment, "line-width", GLib.BindingFlags.SYNC_CREATE); new_segment.insert_before (this, null); segment = new_segment; } if (cycle == current_cycle) { current_segment = segment; } cycles_count++; segment = (Segment?) segment.get_next_sibling (); } link = link.next; } this.current_segment = current_segment; // Update segments without associated cycles. while (segment != null) { segment.cycle = null; segment = (Segment?) segment.get_next_sibling (); } // Update segments span and opacity. var scale_from = this.get_current_scale (); var scale_to = this.get_target_scale (); var position_from = this.get_current_position (); var position_to = this.get_target_position (); if (cycles_count > 1U) { this.fade_in (); } else { this.fade_out (); } if (this._reveal) { this.animate_weights (); this.animate_scale (scale_from, scale_to); this.animate_value (position_from, position_to); } if (this.scale_animation == null) { this.remove_invisible_segments (); } } /** * Find when there will be a long break for the tooltip. */ private void update_long_break_time () { var long_break_time = Ft.Timestamp.UNDEFINED; this._session?.@foreach ( (time_block) => { if (time_block.state == Ft.State.LONG_BREAK && time_block.get_status () == Ft.TimeBlockStatus.SCHEDULED && Ft.Timestamp.is_undefined (long_break_time)) { long_break_time = time_block.start_time; } } ); this.long_break_time = long_break_time; } private void update () { if (this.tick_callback_id != 0) { this.remove_tick_callback (this.tick_callback_id); this.tick_callback_id = 0; } this.update_long_break_time (); this.update_segments (); } private void queue_update () { if (this._reveal && !this.get_mapped ()) { return; } if (this.tick_callback_id != 0) { return; } this.tick_callback_id = this.add_tick_callback ( () => { this.tick_callback_id = 0; this.update (); return GLib.Source.REMOVE; }); } /* * Scale animation */ private void on_scale_animation_done () { this.scale_animation = null; this.update_segments_span (); this.remove_invisible_segments (); } private void animate_scale (float scale_from, float scale_to) { if (scale_to == scale_from) { return; } this.scale = scale_to; if (this.scale_animation != null) { this.scale_animation.pause (); this.scale_animation = null; } if (this.get_mapped () && !scale_from.is_nan () && !scale_to.is_nan ()) { var animation_target = new Adw.CallbackAnimationTarget (this.update_segments_span); this.scale_animation = new Adw.TimedAnimation (this, (double) scale_from, (double) scale_to, SCALE_ANIMATION_DURATION, animation_target); this.scale_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.scale_animation.done.connect (this.on_scale_animation_done); this.scale_animation.play (); } else if (!scale_to.is_nan ()) { this.on_scale_animation_done (); } } /* * Value animation */ private void prepare_value_animation (float position_from, float position_to) { unowned var segment = (Segment?) this.get_first_child (); while (segment != null) { if (segment.span_start < segment.span_end) { var display_value_from = ( (position_from - segment.span_start) / (segment.span_end - segment.span_start)).clamp (0.0f, 1.0f); var display_value_to = ( (position_to - segment.span_start) / (segment.span_end - segment.span_start)).clamp (0.0f, 1.0f); segment.prepare_value_animation ( float.max (display_value_from, segment.display_value), display_value_to); } segment = (Segment?) segment.get_next_sibling (); } } private void finish_value_animation () { unowned var segment = (Segment?) this.get_first_child (); while (segment != null) { segment.finish_value_animation (); segment = (Segment?) segment.get_next_sibling (); } } private void on_value_animation_done () { this.value_animation = null; this.finish_value_animation (); } private void animate_value (float position_from, float position_to) requires (position_from.is_finite ()) requires (position_to.is_finite ()) { if (!this.get_mapped () || (position_to - position_from).abs () < 0.01f) { return; } if (this.value_animation != null) { this.value_animation.pause (); this.value_animation = null; } var segment = this.get_current_segment (); var scale = this.get_current_scale (); var is_forward = position_from < position_to; if (segment == null) { GLib.debug ("Unable to animate value from %.3f to %.3f", position_from, position_to); return; } var animation_duration = (uint)( Math.sqrt ((double)(position_to - position_from).abs () / (double) scale) * (double) VALUE_ANIMATION_DURATION); var animation_target = new Adw.CallbackAnimationTarget ( (position) => { while (segment != null) { if (!is_forward && position < segment.span_start) { segment.set_value_animation_progress (1.0f); segment = (Segment?) segment.get_prev_sibling (); continue; } if (is_forward && position > segment.span_end) { segment.set_value_animation_progress (1.0f); segment = (Segment?) segment.get_next_sibling (); continue; } if (segment.display_value_from == segment.display_value_to) { segment.set_value_animation_progress (1.0f); break; } var segment_position_from = lerpf ( segment.span_start, segment.span_end, segment.display_value_from); var segment_position_to = lerpf ( segment.span_start, segment.span_end, segment.display_value_to); var progress = ((float) position - segment_position_from) / (segment_position_to - segment_position_from); segment.set_value_animation_progress (progress.clamp (0.0f, 1.0f)); break; } }); this.prepare_value_animation (position_from, position_to); this.value_animation = new Adw.TimedAnimation (this, (double) position_from, (double) position_to, animation_duration, animation_target); this.value_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.value_animation.done.connect (this.on_value_animation_done); this.value_animation.play (); } /* * Weights animation */ private void animate_weights () { var segment = (Segment?) this.get_first_child (); // Find the first segment whose weight differs from its current span. // Assume that only one segment may be animated at a time. while (segment != null) { var weight = segment.cycle != null ? (float) segment.cycle.get_weight () : segment.weight; if (weight != segment.weight) { var animation_target = new Adw.CallbackAnimationTarget ( (value) => { segment.weight = (float) value; this.update_segments_span (); }); segment.animate_weight (segment.weight, weight, animation_target); break; } segment = (Segment?) segment.get_next_sibling (); } } /* * Opacity animation (fade-in / fade-out) */ private void on_opacity_animation_done () { this.opacity_animation = null; this.revealing = false; } private void fade_in_internal () { var opacity_from = this.opacity_animation != null ? this.opacity_animation.value : 0.0; var opacity_to = 1.0; if (!this._reveal) { return; } if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } var animation_target = new Adw.CallbackAnimationTarget (this.queue_draw); this.opacity_animation = new Adw.TimedAnimation (this, opacity_from, opacity_to, FADE_IN_DURATION, animation_target); this.opacity_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.opacity_animation.done.connect (this.on_opacity_animation_done); this.opacity_animation.play (); } private void fade_in () { if (this._reveal) { return; } this._reveal = true; this.revealing = true; this.notify_property ("reveal"); if (this.get_mapped ()) { this.fade_in_internal (); } } private void fade_out () { if (!this._reveal) { return; } var opacity_from = this.opacity_animation != null ? this.opacity_animation.value : 1.0; var opacity_to = 0.0; this._reveal = false; this.revealing = false; this.notify_property ("reveal"); if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } if (this.get_mapped ()) { var animation_target = new Adw.CallbackAnimationTarget (this.queue_draw); this.opacity_animation = new Adw.TimedAnimation (this, opacity_from, opacity_to, FADE_OUT_DURATION, animation_target); this.opacity_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.opacity_animation.done.connect (this.on_opacity_animation_done); this.opacity_animation.play (); } else { this.on_opacity_animation_done (); } } /* * Timer / session change handlers */ private void on_session_changed (Ft.Session session) { this.queue_update (); } /** * Timer state may change after rescheduling / updating segments, * so only prod current segment if there's no timeout. */ private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { unowned var segment = (Segment?) this.get_first_child (); unowned var current_time_block = (Ft.TimeBlock?) current_state.user_data; if (current_time_block == null) { return; } while (segment != null) { if (segment.cycle != null && segment.cycle.contains (current_time_block) && !segment.has_timeout ()) { segment.update_timeout (); break; } segment = (Segment?) segment.get_next_sibling (); } } /* * Widget */ public override void map () { this.update (); this._timer.state_changed.connect (this.on_timer_state_changed); base.map (); if (this.revealing) { this.fade_in_internal (); } } public override void unmap () { base.unmap (); if (this.scale_animation != null) { this.scale_animation.pause (); this.scale_animation = null; } if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } if (this.tick_callback_id != 0) { this.remove_tick_callback (this.tick_callback_id); this.tick_callback_id = 0; } this._timer.state_changed.disconnect (this.on_timer_state_changed); this.remove_segments (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var line_width = (int) Math.ceilf (this._line_width); if (orientation == Gtk.Orientation.HORIZONTAL) { minimum = int.max (line_width, MIN_WIDTH); natural = int.max (minimum, for_size); } else { minimum = line_width; natural = minimum; } minimum_baseline = -1; natural_baseline = -1; } /** * All children have same allocation. This way animations can be a little smoother. */ public override void size_allocate (int width, int height, int baseline) { var allocation = Gtk.Allocation () { x = 0, y = 0, width = width, height = height }; unowned var child = this.get_first_child (); while (child != null) { child.allocate_size (allocation, baseline); child = child.get_next_sibling (); } } public override void snapshot (Gtk.Snapshot snapshot) { var width = (float) this.get_width (); var height = (float) this.get_height (); var fade_applied = false; if (this.scale_animation != null) { var scale = this.scale_animation.value; var norm = 1.0 / this.scale_animation.value; var norm_from = 1.0 / this.scale_animation.value_from; var norm_to = 1.0 / this.scale_animation.value_to; var fade_x = width * (float)(double.min (norm_from, norm_to) * scale); var fade_progress = (norm - norm_from) / (norm_to - norm_from); var fade_opacity = norm_from >= norm_to ? (float) fade_progress : (float)(1.0 - fade_progress); if (fade_x < width) { var fade_bounds = Graphene.Rect (); fade_bounds.init (fade_x, 0.0f, width - fade_x, height); snapshot.push_mask (Gsk.MaskMode.INVERTED_ALPHA); snapshot.append_linear_gradient ( fade_bounds, { width, 0.0f }, { fade_x, 0.0f }, { { 0.0f, { 0.0f, 0.0f, 0.0f, fade_opacity }}, { 1.0f, { 0.0f, 0.0f, 0.0f, 0.0f }}, }); snapshot.pop (); fade_applied = true; } } if (this.opacity_animation != null) { snapshot.push_opacity (this.opacity_animation.value); this.snapshot_segments (snapshot); snapshot.pop (); } else if (this._reveal) { this.snapshot_segments (snapshot); } if (fade_applied) { snapshot.pop (); } } public override bool query_tooltip (int x, int y, bool keyboard_tooltip, Gtk.Tooltip tooltip) { var timestamp = int64.max (this._timer.get_last_state_changed_time (), this._timer.get_last_tick_time ()); var remaining = this._timer.is_running () && Ft.Timestamp.is_defined (this.long_break_time) ? Ft.Timestamp.subtract (this.long_break_time, timestamp) : 0; if (remaining > 0) { var seconds = Ft.Timestamp.to_seconds (remaining); var seconds_uint = (uint) Ft.round_seconds (seconds); tooltip.set_markup (_("Long break due in %s").printf ( Ft.format_time (seconds_uint))); // TODO: connect to the timer tick to update the tooltip return true; } else { return base.query_tooltip (x, y, keyboard_tooltip, tooltip); } } public override void dispose () { if (this.scale_animation != null) { this.scale_animation.pause (); this.scale_animation = null; } if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } if (this.value_animation != null) { this.value_animation.pause (); this.value_animation = null; } if (this.tick_callback_id != 0) { this.remove_tick_callback (this.tick_callback_id); this.tick_callback_id = 0; } if (this._session != null) { this._session.changed.disconnect (this.on_session_changed); } this.remove_segments (); this._timer.state_changed.disconnect (this.on_timer_state_changed); this._session_manager = null; this._timer = null; this._session = null; this.current_segment = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/timer-control-buttons.ui000066400000000000000000000143601520625676500304320ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/timer-control-buttons.vala000066400000000000000000000335751520625676500307510ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { private inline Gtk.StackPage? get_stack_page_by_name (Gtk.Stack stack, string name) { return stack.get_page (stack.get_child_by_name (name)); } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/timer/widgets/timer-control-buttons.ui")] public sealed class TimerControlButtons : Gtk.Box, Gtk.Buildable { private const uint FADE_IN_DURATION = 500; private const uint FADE_OUT_DURATION = 500; public bool has_suggested_action { get { return this.center_button.has_css_class ("suggested-action"); } set { if (value) { this.center_button.add_css_class ("suggested-action"); } else { this.center_button.remove_css_class ("suggested-action"); } } } public bool circular { get { return this.center_button.has_css_class ("circular"); } set { if (value) { this.left_button.add_css_class ("circular"); this.center_button.add_css_class ("circular"); this.right_button.add_css_class ("circular"); } else { this.left_button.remove_css_class ("circular"); this.center_button.remove_css_class ("circular"); this.right_button.remove_css_class ("circular"); } } } [GtkChild] private unowned Gtk.Button left_button; [GtkChild] private unowned Gtk.Button center_button; [GtkChild] private unowned Gtk.Button right_button; [GtkChild] private unowned Gtk.Stack left_image_stack; [GtkChild] private unowned Gtk.Stack center_image_stack; [GtkChild] private unowned Gtk.Stack right_image_stack; [GtkChild] private unowned Gtk.Image skip_image; private Ft.SessionManager? session_manager; private Ft.Timer? timer; private ulong timer_state_changed_id = 0; private ulong session_manager_notify_current_session_id = 0; private GLib.List animations; static construct { set_css_name ("timercontrolbuttons"); } construct { this.session_manager = Ft.SessionManager.get_default (); this.timer = Ft.Timer.get_default (); this.update_images (); } private void add_animation (Adw.TimedAnimation animation) { this.animations.append (animation); } private void remove_animation_link (GLib.List? link) { if (link == null) { return; } link.data.pause (); link.data = null; this.animations.delete_link (link); } private void remove_animation (Adw.TimedAnimation animation) { unowned GLib.List link = this.animations.find (animation); if (link != null) { this.remove_animation_link (link); } } private void stop_animations () { unowned GLib.List link; while ((link = this.animations.first ()) != null) { this.remove_animation_link (link); } } private unowned Adw.TimedAnimation? get_animation (Gtk.Widget widget) { unowned GLib.List link = this.animations.first (); while (link != null) { if (link.data.widget == widget) { return link.data; } link = link.next; } return null; } private void fade_in (Gtk.Widget widget, bool animate = true) { var animation = this.get_animation (widget); if (animation != null) { this.remove_animation (animation); } widget.has_tooltip = true; if (!this.get_mapped () || !animate) { widget.opacity = 1.0; return; } if (widget.opacity == 1.0) { return; } var animation_target = new Adw.PropertyAnimationTarget (widget, "opacity"); animation = new Adw.TimedAnimation (widget, widget.opacity, 1.0, FADE_IN_DURATION, animation_target); animation.set_easing (Adw.Easing.EASE_OUT_QUAD); animation.play (); this.add_animation (animation); } private void fade_out (Gtk.Widget widget, bool animate = true) { var animation = this.get_animation (widget); if (animation != null) { this.remove_animation (animation); } widget.has_tooltip = false; if (!this.get_mapped () || !animate) { widget.opacity = 0.0; return; } if (widget.opacity == 0.0) { return; } var animation_target = new Adw.PropertyAnimationTarget (widget, "opacity"); animation = new Adw.TimedAnimation (widget, widget.opacity, 0.0, FADE_OUT_DURATION, animation_target); animation.set_easing (Adw.Easing.EASE_IN_OUT_CUBIC); animation.play (); this.add_animation (animation); } private string get_action_name (string page_name) { switch (page_name) { case "advance": return "session-manager.advance"; case "skip": return "session-manager.advance"; case "skip-break": return "session-manager.skip-break"; case "reset": return "session-manager.reset"; case "stop": return "timer.reset"; default: return "timer.%s".printf (page_name); } } private void update_buttons (bool animate = true) { var current_time_block = this.session_manager.current_time_block; var is_started = this.timer.is_started (); var is_paused = this.timer.is_paused (); var is_finished = this.timer.is_finished (); var is_break = current_time_block != null ? current_time_block.state.is_break () : true; var can_reset = this.session_manager.can_reset (); Gtk.StackPage? left_page = null; Gtk.StackPage? center_page = null; Gtk.StackPage? right_page = null; if (!is_started) { left_page = can_reset ? get_stack_page_by_name (this.left_image_stack, "reset") : null; center_page = get_stack_page_by_name (this.center_image_stack, "start"); assert (center_page != null); if (left_page != null) { this.fade_in (this.left_button, animate); this.fade_out (this.right_button, animate); } else { this.fade_out (this.left_button, animate); this.fade_out (this.right_button, animate); } } else { if (is_paused) { left_page = get_stack_page_by_name (this.left_image_stack, "rewind"); center_page = get_stack_page_by_name (this.center_image_stack, "resume"); right_page = get_stack_page_by_name (this.right_image_stack, "stop"); } else if (is_finished) { left_page = get_stack_page_by_name (this.left_image_stack, "rewind"); center_page = get_stack_page_by_name (this.center_image_stack, "advance"); right_page = get_stack_page_by_name (this.right_image_stack, "stop"); } else { left_page = get_stack_page_by_name (this.left_image_stack, "rewind"); center_page = get_stack_page_by_name (this.center_image_stack, "pause"); right_page = get_stack_page_by_name (this.right_image_stack, "skip"); } assert (left_page != null); assert (center_page != null); assert (right_page != null); this.fade_in (this.left_button, animate); this.fade_in (this.right_button, animate); } if (left_page != null) { if (this.left_button.opacity > 0.0) { this.left_image_stack.visible_child = left_page.child; } else { this.left_image_stack.set_visible_child_full (left_page.name, Gtk.StackTransitionType.NONE); } this.left_button.action_name = this.get_action_name (left_page.name); this.left_button.tooltip_text = left_page.title; } if (center_page != null) { this.center_image_stack.visible_child = center_page.child; this.center_button.action_name = this.get_action_name (center_page.name); this.center_button.tooltip_text = center_page.name == "advance" ? (is_break ? _("Start Pomodoro") : _("Take a break")) : center_page.title; } if (right_page != null) { if (this.right_button.opacity > 0.0) { this.right_image_stack.visible_child = right_page.child; } else { this.right_image_stack.set_visible_child_full (right_page.name, Gtk.StackTransitionType.NONE); } this.right_button.action_name = this.get_action_name (right_page.name); this.right_button.tooltip_text = right_page.name == "skip" ? (is_break ? _("Start Pomodoro") : _("Take a break")) : right_page.title; } this.left_button.can_focus = is_started || can_reset; this.right_button.can_focus = is_started; } private void update_images () { var is_rtl = this.get_direction () == Gtk.TextDirection.RTL; this.skip_image.icon_name = is_rtl ? "timer-skip-symbolic-rtl" : "timer-skip-symbolic"; } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.update_buttons (); } /** * Hide the reset button once the session has been reset, without animation. */ private void on_session_manager_notify_current_session () { var animate = this.session_manager.current_session != null && !this.session_manager.current_session.is_scheduled (); this.update_buttons (animate); } private void connect_signals () { if (this.timer_state_changed_id == 0) { this.timer_state_changed_id = this.timer.state_changed.connect_after (this.on_timer_state_changed); } if (this.session_manager_notify_current_session_id == 0) { this.session_manager_notify_current_session_id = this.session_manager.notify["current-session"].connect (this.on_session_manager_notify_current_session); } } private void disconnect_signals () { if (this.timer_state_changed_id != 0) { this.timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } if (this.session_manager_notify_current_session_id != 0) { this.session_manager.disconnect (this.session_manager_notify_current_session_id); this.session_manager_notify_current_session_id = 0; } } public override void direction_changed (Gtk.TextDirection previous_direction) { base.direction_changed (previous_direction); this.update_images (); } public override void map () { this.update_buttons (false); this.connect_signals (); base.map (); } public override void unmap () { this.stop_animations (); this.disconnect_signals (); base.unmap (); } public override void dispose () { this.stop_animations (); this.disconnect_signals (); this.session_manager = null; this.timer = null; this.animations = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/timer-label.ui000066400000000000000000000126701520625676500263370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/timer-label.vala000066400000000000000000000613231520625676500266440ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/timer/widgets/timer-label.ui")] public class TimerLabel : Gtk.Widget, Gtk.Buildable { private const double BLINK_FADE_VALUE = 0.2; private const uint BLINK_DURATION = 1500; private const uint TRANSITION_DURATION = 500; public unowned Ft.Timer timer { get { return this._timer; } set { if (value == this._timer) { return; } this.disconnect_signals (); this._timer = value; if (this.get_mapped ()) { this.connect_signals (); } } } public bool placeholder_has_hours { get { return this._placeholder_has_hours; } set { if (value == this._placeholder_has_hours) { return; } this._placeholder_has_hours = value; this.placeholder_hours_label.visible = value; this.placeholder_hours_separator_label.visible = value; this.update_css_classes (); this.queue_resize (); } } [GtkChild] private unowned Gtk.Box placeholder_box; [GtkChild] private unowned Ft.MonospaceLabel placeholder_hours_label; [GtkChild] private unowned Ft.MonospaceLabel placeholder_hours_separator_label; [GtkChild] private unowned Ft.MonospaceLabel placeholder_minutes_label; [GtkChild] private unowned Ft.MonospaceLabel placeholder_minutes_separator_label; [GtkChild] private unowned Ft.MonospaceLabel placeholder_seconds_label; [GtkChild] private unowned Gtk.Box box; [GtkChild] private unowned Ft.MonospaceLabel hours_label; [GtkChild] private unowned Ft.MonospaceLabel hours_separator_label; [GtkChild] private unowned Ft.MonospaceLabel minutes_label; [GtkChild] private unowned Ft.MonospaceLabel minutes_separator_label; [GtkChild] private unowned Ft.MonospaceLabel seconds_label; private Ft.Timer _timer; private ulong timer_state_changed_id = 0; private ulong timer_tick_id = 0; private Adw.TimedAnimation? crossfade_animation; private Adw.TimedAnimation? blink_animation; private Adw.TimedAnimation? hours_animation; private double reference_width_lower = 0.0; private double reference_width_upper = 0.0; private double reference_height = 0.0; private double reference_baseline = 0.0; private bool faded_in = false; private bool _placeholder_has_hours = false; private bool has_hours = false; private double scale = 1.0; static construct { set_css_name ("timerlabel"); } construct { this._timer = Ft.Timer.get_default (); this.set_default_direction_ltr (); } private void set_default_direction_ltr () { this.placeholder_box.set_direction (Gtk.TextDirection.LTR); this.placeholder_hours_label.set_direction (Gtk.TextDirection.LTR); this.placeholder_hours_separator_label.set_direction (Gtk.TextDirection.LTR); this.placeholder_minutes_label.set_direction (Gtk.TextDirection.LTR); this.placeholder_minutes_separator_label.set_direction (Gtk.TextDirection.LTR); this.placeholder_seconds_label.set_direction (Gtk.TextDirection.LTR); this.box.set_direction (Gtk.TextDirection.LTR); this.hours_label.set_direction (Gtk.TextDirection.LTR); this.hours_separator_label.set_direction (Gtk.TextDirection.LTR); this.minutes_label.set_direction (Gtk.TextDirection.LTR); this.minutes_separator_label.set_direction (Gtk.TextDirection.LTR); this.seconds_label.set_direction (Gtk.TextDirection.LTR); } private double get_crossfade_progress () { if (this.crossfade_animation != null) { return this.crossfade_animation.value; } return this.faded_in ? 1.0 : 0.0; } private void update_children_scale () { this.placeholder_hours_label.scale = this.scale; this.placeholder_hours_separator_label.scale = this.scale; this.placeholder_minutes_label.scale = this.scale; this.placeholder_minutes_separator_label.scale = this.scale; this.placeholder_seconds_label.scale = this.scale; this.hours_label.scale = this.scale; this.hours_separator_label.scale = this.scale; this.minutes_label.scale = this.scale; this.minutes_separator_label.scale = this.scale; this.seconds_label.scale = this.scale; } private void stop_hours_animation () { if (this.hours_animation != null) { this.hours_animation.pause (); this.hours_animation = null; } } /** * TODO: hours animation is not smooth. * * When animating labels scale the labels are jittery. It's smoother to keep labels at constant scale during * animation, but during snapshot children are pixelated. Either we could render glyphs directly in the * snapshot, or render children onto a texture to have more interpolation options. */ private void start_hours_animation () { var has_hours = this.get_has_hours (); var progress = !has_hours ? 1.0 : 0.0; // assume previously it was reverse if (this.hours_animation != null) { progress = this.hours_animation.value; this.hours_animation.pause (); this.hours_animation = null; } var animation_target = new Adw.CallbackAnimationTarget (this.queue_resize); this.hours_animation = new Adw.TimedAnimation (this, progress, has_hours ? 1.0 : 0.0, 300, animation_target); this.hours_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.hours_animation.done.connect (this.stop_hours_animation); this.hours_animation.play (); } private void stop_crossfade_animation () { if (this.crossfade_animation != null) { this.crossfade_animation.pause (); this.crossfade_animation = null; } this.placeholder_box.set_child_visible (!this.faded_in); this.box.set_child_visible (this.faded_in); this.queue_allocate (); } private void fade_in () { var crossfade_progress = this.get_crossfade_progress (); if (this.faded_in) { return; } if (this.crossfade_animation != null) { this.crossfade_animation.pause (); this.crossfade_animation = null; } var animation_target = new Adw.CallbackAnimationTarget (this.queue_draw); this.crossfade_animation = new Adw.TimedAnimation (this, crossfade_progress, 1.0, TRANSITION_DURATION, animation_target); this.crossfade_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.crossfade_animation.done.connect (this.stop_crossfade_animation); this.crossfade_animation.play (); this.placeholder_box.set_child_visible (true); this.box.set_child_visible (true); this.box.opacity = 1.0; if (this.has_hours != this._placeholder_has_hours && this.hours_animation == null) { this.start_hours_animation (); } this.faded_in = true; } private void fade_out () { var crossfade_progress = this.get_crossfade_progress (); if (!this.faded_in) { return; } if (this.crossfade_animation != null) { this.crossfade_animation.pause (); this.crossfade_animation = null; } if (this.blink_animation != null) { this.blink_animation.pause (); this.blink_animation = null; } var animation_target = new Adw.CallbackAnimationTarget (this.queue_draw); this.crossfade_animation = new Adw.TimedAnimation (this, crossfade_progress, 0.0, TRANSITION_DURATION, animation_target); this.crossfade_animation.set_easing (Adw.Easing.EASE_IN_OUT_CUBIC); this.crossfade_animation.done.connect (this.stop_crossfade_animation); this.crossfade_animation.play (); this.placeholder_box.set_child_visible (true); this.box.set_child_visible (true); if (this.has_hours != this._placeholder_has_hours && this.hours_animation == null) { this.start_hours_animation (); } this.faded_in = false; } private void update_css_classes () { if (this._placeholder_has_hours) { this.placeholder_box.add_css_class ("with-hours"); } else { this.placeholder_box.remove_css_class ("with-hours"); } if (this.has_hours) { this.box.add_css_class ("with-hours"); } else { this.box.remove_css_class ("with-hours"); } } private void update_remaining_time (int64 timestamp) { var remaining = this._timer.calculate_remaining (timestamp); var remaining_uint = Ft.Timestamp.to_seconds_uint (remaining); var has_hours = remaining_uint >= 3600; if (this.has_hours != has_hours) { this.hours_label.visible = has_hours; this.hours_separator_label.visible = has_hours; this.has_hours = has_hours; this.update_css_classes (); if (this.faded_in) { this.start_hours_animation (); } } if (has_hours) { this.hours_label.text = (remaining_uint / 3600).to_string (); remaining_uint = remaining_uint % 3600; } this.minutes_label.text = "%02u".printf (remaining_uint / 60); this.seconds_label.text = "%02u".printf (remaining_uint % 60); } private bool should_blink () { if (this._timer.user_data == null) { return false; } return this._timer.is_paused () || this._timer.is_finished () || !this._timer.is_started (); } private void stop_blinking_animation () { if (this.blink_animation == null) { return; } this.blink_animation.pause (); this.blink_animation = null; // Animate opacity back to a baseline value. if (this.get_mapped () && this.box.opacity != 1.0) { var animation_target = new Adw.PropertyAnimationTarget (this.box, "opacity"); this.blink_animation = new Adw.TimedAnimation (this.box, this.box.opacity, 1.0, TRANSITION_DURATION, animation_target); this.blink_animation.easing = Adw.Easing.EASE_OUT_QUAD; this.blink_animation.follow_enable_animations_setting = false; this.blink_animation.done.connect (this.stop_blinking_animation); this.blink_animation.play (); } else { this.box.opacity = 1.0; } } private void start_blinking_animation () { if (!this.get_mapped ()) { return; } if (this.blink_animation != null && this.blink_animation.alternate && this.blink_animation.state == Adw.AnimationState.PLAYING) { return; } if (this.blink_animation != null) { this.blink_animation.pause (); this.blink_animation = null; } var animation_target = new Adw.PropertyAnimationTarget (this.box, "opacity"); this.blink_animation = new Adw.TimedAnimation (this.box, this.box.opacity, BLINK_FADE_VALUE, BLINK_DURATION, animation_target); this.blink_animation.alternate = this.box.opacity == 1.0; this.blink_animation.follow_enable_animations_setting = false; if (this.blink_animation.value_from <= BLINK_FADE_VALUE) { this.blink_animation.value_to = 1.0; } if (this.blink_animation.alternate) { this.blink_animation.repeat_count = uint.MAX; this.blink_animation.easing = Adw.Easing.EASE_IN_OUT_CUBIC; } else { this.blink_animation.easing = Adw.Easing.EASE_OUT_QUAD; this.blink_animation.done.connect (this.start_blinking_animation); } this.blink_animation.play (); } /** * Sync all of things with the timer state */ private void update (int64 timestamp = Ft.Timestamp.UNDEFINED) { if (this._timer.user_data != null) { this.update_remaining_time (timestamp); this.fade_in (); } else { this.fade_out (); } if (this.should_blink ()) { this.start_blinking_animation (); } else { this.stop_blinking_animation (); } this.queue_resize (); } private void connect_signals () { if (this.timer_tick_id == 0) { this.timer_tick_id = this._timer.tick.connect (this.on_timer_tick); } if (this.timer_state_changed_id == 0) { this.timer_state_changed_id = this._timer.state_changed.connect_after (this.on_timer_state_changed); } } private void disconnect_signals () { if (this.timer_tick_id != 0) { this._timer.disconnect (this.timer_tick_id); this.timer_tick_id = 0; } if (this.timer_state_changed_id != 0) { this._timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } } private void on_timer_tick (int64 timestamp) { this.update_remaining_time (timestamp); } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { if (this.get_mapped ()) { this.update (this._timer.get_last_state_changed_time ()); } } /** * To estimate font scale we need to measure layout at reference scale. */ private void ensure_reference_size () { if (this.reference_width_lower == 0.0) { var layout = this.create_pango_layout ("00:00"); var layout_width = 0; var layout_height = 0; layout.get_size (out layout_width, out layout_height); this.reference_width_lower = (double) layout_width / (double) Pango.SCALE; this.reference_height = (double) layout_height / (double) Pango.SCALE; this.reference_baseline = (double) layout.get_baseline () / (double) Pango.SCALE; layout.set_text ("0:00:00", 7); layout.get_size (out layout_width, out layout_height); this.reference_width_upper = (double) layout_width / (double) Pango.SCALE; } } private void invalidate_reference_size () { this.reference_width_lower = 0.0; this.reference_width_upper = 0.0; this.reference_height = 0.0; this.reference_baseline = 0.0; } private bool get_has_hours () { return this.timer.is_started () ? this.has_hours : this._placeholder_has_hours; } private double get_reference_width () { if (this.hours_animation != null) { return Adw.lerp (this.reference_width_lower, this.reference_width_upper, this.hours_animation.value); } return this.faded_in ? (double) (this.has_hours ? this.reference_width_upper : this.reference_width_lower) : (double) (this._placeholder_has_hours ? this.reference_width_upper : this.reference_width_lower); } // public override void css_changed (Gtk.CssStyleChange change) // { // base.css_changed (change); // // this.invalidate_reference_size (); // NOTE: this is triggered on unfocus // } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } /** * Estimate size. * * Interpolate between two children and with-hours / without-hours. */ public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { this.ensure_reference_size (); var reference_width = this.get_reference_width (); var reference_height = this.reference_height; var reference_baseline = this.reference_baseline; var scale = 1.0; if (for_size != -1 && this.halign == Gtk.Align.FILL) { scale = orientation == Gtk.Orientation.HORIZONTAL ? (double) for_size / reference_height : (double) for_size / reference_width; } if (orientation == Gtk.Orientation.HORIZONTAL) { natural = (int) Math.round (scale * reference_width); natural_baseline = -1; } else { natural = (int) Math.round (scale * reference_height); natural_baseline = (int) Math.round (scale * reference_baseline); } minimum = natural; minimum_baseline = natural_baseline; } public override void size_allocate (int width, int height, int baseline) { var placeholder_allocation = Gtk.Allocation (); var allocation = Gtk.Allocation (); this.ensure_reference_size (); var scale = this.halign == Gtk.Align.FILL ? (double) width / this.get_reference_width () : 1.0; if (this.scale != scale) { this.scale = scale; this.update_children_scale (); } this.placeholder_box.measure ( Gtk.Orientation.VERTICAL, -1, null, out placeholder_allocation.height, null, null); this.placeholder_box.measure ( Gtk.Orientation.HORIZONTAL, -1, null, out placeholder_allocation.width, null, null); this.box.measure (Gtk.Orientation.VERTICAL, -1, null, out allocation.height, null, null); this.box.measure (Gtk.Orientation.HORIZONTAL, -1, null, out allocation.width, null, null); switch (this.halign) { case Gtk.Align.START: placeholder_allocation.x = 0; allocation.x = 0; break; case Gtk.Align.END: placeholder_allocation.x = width - placeholder_allocation.width; allocation.x = width - allocation.width; break; case Gtk.Align.CENTER: case Gtk.Align.FILL: placeholder_allocation.x = (width - placeholder_allocation.width) / 2; allocation.x = (width - allocation.width) / 2; break; default: assert_not_reached (); } placeholder_allocation.y = (height - placeholder_allocation.height) / 2; allocation.y = (height - allocation.height) / 2; this.placeholder_box.allocate_size (placeholder_allocation, baseline); this.box.allocate_size (allocation, baseline); } public override void snapshot (Gtk.Snapshot snapshot) { if (this.crossfade_animation != null) { snapshot.push_cross_fade (this.crossfade_animation.value); this.snapshot_child (this.placeholder_box, snapshot); snapshot.pop (); this.snapshot_child (this.box, snapshot); snapshot.pop (); } else { if (!this.faded_in) { this.snapshot_child (this.placeholder_box, snapshot); } else { this.snapshot_child (this.box, snapshot); } } } public override void map () { this.update (this._timer.get_last_tick_time ()); this.connect_signals (); base.map (); if (this.should_blink ()) { this.start_blinking_animation (); } } public override void unmap () { this.disconnect_signals (); this.stop_blinking_animation (); this.stop_crossfade_animation (); this.stop_hours_animation (); base.unmap (); } public override void unroot () { base.unroot (); this.invalidate_reference_size (); } public override void dispose () { if (this.placeholder_box != null) { this.placeholder_box.unparent (); } if (this.box != null) { this.box.unparent (); } this._timer = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/timer/widgets/timer-progress-bar.vala000066400000000000000000001057261520625676500302010ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public enum TimerProgressShape { BAR, RING } public sealed class TimerProgressBar : Gtk.Widget { private const float MIN_LINE_WIDTH = 6.0f; private const float MAX_LINE_WIDTH = 8.0f; private const int MIN_WIDTH = 100; private const uint TIMEOUT_RESOLUTION = 2U; private const uint MIN_TIMEOUT_INTERVAL = 25; // 40Hz private const uint FADE_IN_DURATION = 500; private const uint FADE_OUT_DURATION = 500; public Ft.Timer timer { get { return this._timer; } construct { this._timer = value != null ? value : Ft.Timer.get_default (); } } [CCode (notify = false)] public Ft.TimerProgressShape shape { get { return this._shape; } construct { Ft.Gizmo through; Ft.Gizmo highlight; this._shape = value; switch (this._shape) { case Ft.TimerProgressShape.BAR: through = new Ft.Gizmo ( TimerProgressBar.measure_bar_cb, null, TimerProgressBar.snapshot_bar_through_cb, null, null, null); highlight = new Ft.Gizmo ( TimerProgressBar.measure_bar_cb, null, TimerProgressBar.snapshot_bar_highlight_cb, null, null, null); break; case Ft.TimerProgressShape.RING: through = new Ft.Gizmo ( TimerProgressBar.measure_ring_cb, null, TimerProgressBar.snapshot_ring_through_cb, null, null, null); highlight = new Ft.Gizmo ( TimerProgressBar.measure_ring_cb, null, TimerProgressBar.snapshot_ring_highlight_cb, null, null, null); break; default: assert_not_reached (); } through.focusable = false; through.add_css_class ("through"); through.set_parent (this); highlight.focusable = false; highlight.add_css_class ("highlight"); highlight.set_parent (this); this.through = through; this.highlight = highlight; this.queue_resize (); } } [CCode (notify = false)] public float line_width { get { return this._line_width; } set { if (this._line_width != value) { this._line_width = value; this.line_width_set = true; this.notify_property ("line-width"); this.queue_allocate (); } } } [CCode (notify = false)] public bool line_width_set { get { return this._line_width_set; } set { if (this._line_width_set != value) { this._line_width_set = value; this.notify_property ("line-width-set"); this.queue_allocate (); } } } public float display_value { get { return (float) this._display_value; } } private Ft.TimerProgressShape _shape = Ft.TimerProgressShape.BAR; private Ft.Timer _timer; private float _line_width = MIN_LINE_WIDTH; private bool _line_width_set = false; private double _display_value = 0.0; private double display_value_from = 0.0; private double display_value_to = 0.0; private int64 last_display_time = Ft.Timestamp.UNDEFINED; private Adw.TimedAnimation? value_animation = null; private Adw.TimedAnimation? opacity_animation = null; private unowned Ft.Gizmo through = null; private unowned Ft.Gizmo highlight = null; private ulong tick_id = 0U; private uint tick_callback_id = 0U; private uint timeout_id = 0U; private uint timeout_interval = 0U; private uint timeout_inhibit_count = 0U; private float radius; private float line_cap_radius; private float line_cap_angle; static construct { set_css_name ("timerprogressbar"); } private inline int64 get_current_time () { return this._timer.is_running () ? this._timer.get_current_time (this.get_frame_clock ().get_frame_time ()) : this._timer.get_last_state_changed_time (); } /** * Stop the timeout callbacks. Prioritise animations over the timeout. It's redundant to * run both. */ private void inhibit_timeout () { this.timeout_inhibit_count++; this.stop_timeout (); } private void uninhibit_timeout () { if (this.timeout_inhibit_count > 0) { this.timeout_inhibit_count--; if (this.timeout_inhibit_count == 0 && this._timer.is_running ()) { this.start_timeout (); } } } private void start_timeout () { if (this.timeout_inhibit_count > 0) { return; } var timeout_interval = this.calculate_timeout_interval (); if (timeout_interval < MIN_TIMEOUT_INTERVAL) { timeout_interval = 0U; } if (this.timeout_interval != timeout_interval) { this.timeout_interval = timeout_interval; this.stop_timeout (); } if (this.tick_id == 0 && timeout_interval > 0 && timeout_interval > 500) { this.tick_id = this._timer.tick.connect ( (timestamp) => { this.highlight.queue_draw (); }); } else if (this.timeout_id == 0 && timeout_interval > 0) { this.timeout_id = GLib.Timeout.add ( timeout_interval, () => { this.highlight.queue_draw (); return GLib.Source.CONTINUE; }); GLib.Source.set_name_by_id (this.timeout_id, "Ft.TimerProgressBar.queue_draw"); } else if (this.tick_callback_id == 0 && timeout_interval == 0) { this.tick_callback_id = this.add_tick_callback ( () => { this.highlight.queue_draw (); return GLib.Source.CONTINUE; }); } } private void stop_timeout () { if (this.tick_id != 0) { this._timer.disconnect (this.tick_id); this.tick_id = 0; } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } if (this.tick_callback_id != 0) { this.remove_tick_callback (this.tick_callback_id); this.tick_callback_id = 0; } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { var timestamp = this._timer.get_last_state_changed_time (); var is_ring = this._shape == Ft.TimerProgressShape.RING; if (!current_state.is_finished () && previous_state.is_finished () && is_ring) { this.fade_in (); } else if (current_state.user_data != null && previous_state.user_data == null) { this.fade_in (); } else if (!current_state.is_started () && previous_state.is_started ()) { this.fade_out (); } if (this.opacity_animation == null && (timestamp - this.last_display_time) < 5 * Ft.Interval.SECOND) { var value_from = this._display_value; var value_to = this._timer.calculate_progress (timestamp); this.animate_value (value_from, value_to); } if (current_state.is_running ()) { this.start_timeout (); } else { this.stop_timeout (); } this.highlight.queue_draw (); } private void on_opacity_animation_done () { this.opacity_animation = null; this.uninhibit_timeout (); } private void on_value_animation_done () { this.value_animation = null; this.uninhibit_timeout (); } private float calculate_line_width (int size) { // HACK: sizing is hard-coded for the TimerView; perhaps it's not the best place var size_float = (float) Math.roundf ((float) size); var min_size = 300.0f; var max_size = 450.0f; var t = ((size_float - min_size) / (max_size - min_size)).clamp (0.0f, 1.0f); return Math.roundf (lerpf (MIN_LINE_WIDTH, MAX_LINE_WIDTH, t)); } private uint calculate_timeout_interval () { int64 distance; switch (this._shape) { case Ft.TimerProgressShape.BAR: distance = (int64) this.get_width (); break; case Ft.TimerProgressShape.RING: distance = (int64) Math.ceil (2.0 * Math.PI * (double) this.radius); break; default: assert_not_reached (); } distance *= TIMEOUT_RESOLUTION; return distance > 0 ? Ft.Timestamp.to_milliseconds_uint (this._timer.duration / distance) : 0; } private uint calculate_animation_duration (double value_from, double value_to) { switch (this._shape) { case Ft.TimerProgressShape.BAR: return (uint)(Math.sqrt ((value_to - value_from).abs ()) * 500.0); case Ft.TimerProgressShape.RING: return (uint)(Math.sqrt ((value_to - value_from).abs ()) * 1000.0); default: assert_not_reached (); } } private void fade_in () { var opacity_from = this.opacity_animation != null ? this.opacity_animation.value : 0.0; var opacity_to = 1.0; if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; this.uninhibit_timeout (); } if (this.get_mapped ()) { this.inhibit_timeout (); var animation_target = new Adw.CallbackAnimationTarget (this.highlight.queue_draw); this.opacity_animation = new Adw.TimedAnimation (this.highlight, opacity_from, opacity_to, FADE_IN_DURATION, animation_target); this.opacity_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.opacity_animation.done.connect (this.on_opacity_animation_done); this.opacity_animation.play (); } } private void fade_out () { var opacity_from = this.opacity_animation != null ? this.opacity_animation.value : 1.0; var opacity_to = 0.0; if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; this.uninhibit_timeout (); } if (this.get_mapped ()) { this.inhibit_timeout (); var animation_target = new Adw.CallbackAnimationTarget (this.highlight.queue_draw); this.opacity_animation = new Adw.TimedAnimation (this.highlight, opacity_from, opacity_to, FADE_OUT_DURATION, animation_target); this.opacity_animation.set_easing (Adw.Easing.EASE_IN_OUT_CUBIC); this.opacity_animation.done.connect (this.on_opacity_animation_done); this.opacity_animation.play (); } } private void animate_value (double value_from, double value_to) requires (value_from.is_finite ()) requires (value_to.is_finite ()) { if ((value_from - value_to).abs () < 0.01) { return; } if (this.value_animation != null) { this.value_animation.pause (); this.value_animation = null; this.uninhibit_timeout (); } if (this.get_mapped ()) { this.inhibit_timeout (); var animation_duration = this.calculate_animation_duration (value_from, value_to); var animation_target = new Adw.CallbackAnimationTarget (this.highlight.queue_draw); this.value_animation = new Adw.TimedAnimation (this.highlight, 0.0, 1.0, animation_duration, animation_target); this.value_animation.set_easing (this._timer.is_paused () ? Adw.Easing.EASE_IN_OUT_CUBIC : Adw.Easing.EASE_OUT_QUAD); this.value_animation.done.connect (this.on_value_animation_done); this.value_animation.play (); this.display_value_from = value_from; this.display_value_to = value_to; } } /* * Bar shape */ private static void measure_bar_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.measure_bar (gizmo, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } } private static void snapshot_bar_through_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.snapshot_bar_through (gizmo, snapshot); } } private static void snapshot_bar_highlight_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.snapshot_bar_highlight (gizmo, snapshot); } } private void measure_bar (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { if (orientation == Gtk.Orientation.VERTICAL) { minimum = (int) Math.ceilf (this._line_width); natural = minimum; } else { minimum = MIN_WIDTH; natural = minimum; } minimum_baseline = -1; natural_baseline = -1; } private void snapshot_bar_through (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var width = (float) gizmo.get_width (); var height = (float) gizmo.get_height (); var color = gizmo.get_color (); var through_width = width; var through_height = this._line_width; var through_x = 0.0f; var through_y = (height - through_height) / 2.0f; var through_bounds = Graphene.Rect (); var through_outline = Gsk.RoundedRect (); through_bounds.init (through_x, through_y, through_width, through_height); through_outline.init_from_rect (through_bounds, through_height / 2.0f); snapshot.push_rounded_clip (through_outline); snapshot.append_color (color, through_bounds); snapshot.pop (); } private void snapshot_bar_highlight (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { double display_value; var opacity = this.opacity_animation != null ? this.opacity_animation.value : 1.0; var timestamp = this.get_current_time (); if (this.opacity_animation == null || this.opacity_animation.value_to > 0.0) { display_value = this._timer.user_data != null ? this._timer.calculate_progress (this.get_current_time ()) : 0.0; } else { display_value = this._display_value; } if (this.value_animation != null) { display_value = lerp (this.display_value_from, display_value, this.value_animation.value); } if (display_value <= 0.0 || opacity == 0.0) { this._display_value = 0.0; this.last_display_time = timestamp; return; // Nothing to draw } var width = (float) gizmo.get_width (); var height = (float) gizmo.get_height (); var color = gizmo.get_color (); var clip_applied = false; var opacity_applied = false; if (opacity < 1.0) { snapshot.push_opacity (this.opacity_animation.value); opacity_applied = true; } var highlight_width = width * float.min ((float) display_value, 1.0f); var highlight_height = this._line_width; var highlight_x = 0.0f; var highlight_y = (height - highlight_height) / 2.0f; var highlight_bounds = Graphene.Rect (); var highlight_outline = Gsk.RoundedRect (); if (this.get_direction () == Gtk.TextDirection.RTL) { highlight_x = width - highlight_x - highlight_width; } if (highlight_width < highlight_height) { var clip_bounds = Graphene.Rect (); var clip_outline = Gsk.RoundedRect (); clip_bounds.init (0.0f, highlight_y, width, highlight_height); clip_outline.init_from_rect (clip_bounds, highlight_height / 2.0f); snapshot.push_rounded_clip (clip_outline); highlight_x -= highlight_height - highlight_width; highlight_width = highlight_height; clip_applied = true; } highlight_bounds.init (highlight_x, highlight_y, highlight_width, highlight_height); highlight_outline.init_from_rect (highlight_bounds, highlight_height / 2.0f); snapshot.push_rounded_clip (highlight_outline); snapshot.append_color (color, highlight_bounds); snapshot.pop (); if (clip_applied) { snapshot.pop (); } if (opacity_applied) { snapshot.pop (); } this._display_value = display_value; this.last_display_time = timestamp; } /* * Ring shape */ private static void measure_ring_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.measure_ring (gizmo, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } } private static void snapshot_ring_through_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.snapshot_ring_through (gizmo, snapshot); } } private static void snapshot_ring_highlight_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { unowned var self = (Ft.TimerProgressBar) gizmo.parent; if (self != null) { self.snapshot_ring_highlight (gizmo, snapshot); } } private void measure_ring (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = int.max (for_size, MIN_WIDTH); natural = minimum; minimum_baseline = -1; natural_baseline = -1; } private void snapshot_ring_through (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var color = gizmo.get_color (); var origin = Graphene.Point () { x = (float) gizmo.get_width () / 2.0f, y = (float) gizmo.get_height () / 2.0f }; var path_builder = new Gsk.PathBuilder (); path_builder.add_circle (origin, this.radius); var stroke = new Gsk.Stroke (this._line_width); snapshot.append_stroke (path_builder.to_path (), stroke, color); } private void snapshot_ring_highlight (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { double display_value; var opacity = this.opacity_animation != null ? this.opacity_animation.value : 1.0; var timestamp = this.get_current_time (); if (this.opacity_animation == null || this.opacity_animation.value_to > 0.0) { display_value = this._timer.user_data != null && !this._timer.is_finished () ? this._timer.calculate_progress (timestamp) : 1.0; } else { display_value = this._display_value; } if (this.value_animation != null) { display_value = lerp (this.display_value_from, display_value, this.value_animation.value); } if (display_value >= 1.0 || opacity == 0.0) { this._display_value = 1.0; this.last_display_time = timestamp; return; // Nothing to draw } var color = gizmo.get_color (); var origin = Graphene.Point () { x = (float) gizmo.get_width () / 2.0f, y = (float) gizmo.get_height () / 2.0f }; var path_builder = new Gsk.PathBuilder (); var clip_applied = false; var opacity_applied = false; if (opacity < 1.0) { snapshot.push_opacity (opacity); opacity_applied = true; } // Draw a circular arc representing remaining time. The arc starts at the top // of the circle (-90°) and sweeps counter-clockwise as time progresses. // For edge cases (sweep > 360° or < 0°), we apply clipping and adjust the starting // point to handle line cap rendering correctly. `svg_arc_to ()` requires an arc // endpoint, so we do some trigonometry. if (display_value < 0.001) { path_builder.add_circle (origin, this.radius); } else if (display_value < 0.999) { var sweep_angle = (2.0 * Math.PI + this.line_cap_angle) * (1.0 - display_value) - this.line_cap_angle; if (sweep_angle > 2.0 * Math.PI) { path_builder.move_to (origin.x + this.radius, origin.y); clip_applied = true; } else if (sweep_angle < 0.0) { path_builder.move_to (origin.x - this.radius, origin.y); clip_applied = true; } else { path_builder.move_to (origin.x, origin.y - this.radius); } if (clip_applied) { var clip_bounds = Graphene.Rect (); var clip_outline = Gsk.RoundedRect (); clip_bounds.init (origin.x - this._line_width / 2.0f, origin.y - this.radius - this._line_width / 2.0f, this._line_width, this._line_width); clip_outline.init_from_rect (clip_bounds, this._line_width / 2.0f); snapshot.push_rounded_clip (clip_outline); } float sin_angle, cos_angle; Math.sincosf ((float)(-Math.PI_2 + sweep_angle), out sin_angle, out cos_angle); path_builder.svg_arc_to (this.radius, this.radius, 0.0f, sweep_angle > Math.PI, true, origin.x + this.radius * cos_angle, origin.y + this.radius * sin_angle); } var stroke = new Gsk.Stroke (this._line_width); stroke.set_line_cap (Gsk.LineCap.ROUND); snapshot.append_stroke (path_builder.to_path (), stroke, color); if (clip_applied) { snapshot.pop (); } if (opacity_applied) { snapshot.pop (); } this._display_value = display_value; this.last_display_time = timestamp; } /* * Widget */ public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var minimum_for_size = 0; this.through.measure (get_opposite_orientation (orientation), -1, out minimum_for_size, null, null, null); this.through.measure (orientation, int.max (minimum_for_size, for_size), out minimum, out natural, null, null); minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { float line_width; if (!this._line_width_set) { switch (this._shape) { case Ft.TimerProgressShape.BAR: line_width = this.calculate_line_width (width); break; case Ft.TimerProgressShape.RING: var size = int.min (width, height); line_width = this.calculate_line_width (size); this.radius = ((float) size - line_width) / 2.0f; this.line_cap_radius = line_width / 2.0f; this.line_cap_angle = Math.atan2f (2.0f * this.line_cap_radius, this.radius); break; default: assert_not_reached (); } } else { line_width = this._line_width; } if (this._line_width != line_width) { this._line_width = line_width; this.notify_property ("line-width"); } this.through.allocate (width, height, baseline, null); this.highlight.allocate (width, height, baseline, null); } public override void snapshot (Gtk.Snapshot snapshot) { this.snapshot_child (this.through, snapshot); this.snapshot_child (this.highlight, snapshot); } public override void map () { base.map (); this._timer.state_changed.connect (this.on_timer_state_changed); this.timeout_inhibit_count = 0; if (this._timer.is_running ()) { this.start_timeout (); } } public override void unmap () { this.stop_timeout (); this._timer.state_changed.disconnect (this.on_timer_state_changed); if (this.value_animation != null) { this.value_animation.pause (); this.value_animation = null; } if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } base.unmap (); } public override void dispose () { this.stop_timeout (); if (this.value_animation != null) { this.value_animation.pause (); this.value_animation = null; } if (this.opacity_animation != null) { this.opacity_animation.pause (); this.opacity_animation = null; } this.through.unparent (); this.highlight.unparent (); this.through = null; this.highlight = null; this._timer = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/widgets/000077500000000000000000000000001520625676500224555ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/main/widgets/size-stack.vala000066400000000000000000000450711520625676500254060ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public class SizeStackPage : GLib.Object { public Gtk.Widget child { get; construct; } public string name { get; set; } public bool resizable { get; set; default = true; } internal unowned Gtk.Widget? last_focus = null; internal int last_width = -1; internal int last_height = -1; public SizeStackPage (Gtk.Widget child) { GLib.Object ( child: child ); } public override void dispose () { this.last_focus = null; base.dispose (); } } /** * Container for transitioning between widgets of varying sizes. * * The purpose is to swap widgets of varying complexity or just different variants. * It's tailored for interpolating window size - origin of the transition is at top left corner. */ public class SizeStack : Gtk.Widget, Gtk.Buildable { public unowned Gtk.Widget? visible_child { get { return this.current_page?.child; } set { unowned var page = this.find_page_by_child (value); if (page == null) { GLib.warning ("Given child of type '%s' not found in PomodoroSizeStack", value.get_type ().name ()); return; } this.select_page (page, true); } } public string visible_child_name { get { return this.current_page != null ? this.current_page.name : ""; } set { unowned var page = this.find_page_by_name (value); if (page == null) { GLib.warning ("Page with name '%s' not found in PomodoroSizeStack", value); return; } this.select_page (page, true); } } // The animation duration, in milliseconds. public uint transition_duration { get; set; default = 500; } private GLib.List pages = null; private Adw.TimedAnimation? transition_animation = null; private unowned Ft.SizeStackPage? current_page = null; private unowned Ft.SizeStackPage? previous_page = null; private bool size_request_set = false; private unowned Ft.SizeStackPage? find_page_by_child (Gtk.Widget child) { unowned var link = this.pages.first (); while (link != null) { if (link.data.child == child) { return link.data; } link = link.next; } return null; } private unowned Ft.SizeStackPage? find_page_by_name (string name) { unowned var link = this.pages.first (); while (link != null) { if (link.data.name == name) { return link.data; } link = link.next; } return null; } private float get_transition_progress () { return this.transition_animation != null ? (float) this.transition_animation.value : 1.0f; } private void stop_resizing_window () { var window = this.get_root () as Gtk.Window; var resizable = this.current_page != null ? this.current_page.resizable : true; if (window == null) { return; } window.halign = Gtk.Align.FILL; window.valign = Gtk.Align.FILL; if (window.resizable != resizable) { window.resizable = resizable; /* // HACK: Workaround for not being able to resize window after the transition. // When changing `resizable`, GTK calls gdk_toplevel_present() which can cause // the compositor to restore a previously remembered window size. // Keep size request set for a short time to prevent GTK from changing size var width = window.get_width (); var height = window.get_height (); window.set_size_request (width, height); window.resizable = resizable; var timeout_id = GLib.Timeout.add (100, () => { window.set_size_request (-1, -1); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (timeout_id, "Ft.SizeStack.on_transition_animation_done"); */ } else { // HACK: There's a glitch with recent GTK+ / Mutter on Wayland, that window size // shrinks to minimum size when initiating a resize. The workaround enforces a // minimum size which we lift once user starts resizing the window. It's not great, // because the user can't shrink window down at first. var width = window.get_width (); var height = window.get_height (); window.set_size_request (width, height); this.add_tick_callback ( () => { this.size_request_set = true; return GLib.Source.REMOVE; }); } } private void start_resizing_window () { var window = this.get_root () as Gtk.Window; if (window == null) { return; } window.halign = Gtk.Align.START; window.valign = Gtk.Align.START; window.set_size_request (-1, -1); window.resizable = true; } /** * In GTK4, Window insists on preserving its remembered size. We need to invalidate it. * See `gtk_window_compute_default_size`. */ private inline void invalidate_window_default_size () { var window = this.get_root () as Gtk.Window; window?.set_default_size (-1, -1); } /** * Call it after setting `visible_child` and `last_visible_child`. */ private void start_transition (uint transition_duration) requires (this.current_page != null && this.previous_page != null) { if (this.transition_animation != null) { this.transition_animation.pause (); this.transition_animation = null; } this.start_resizing_window (); var animation_target = new Adw.CallbackAnimationTarget ( (value) => { this.queue_resize (); this.invalidate_window_default_size (); }); var animation = new Adw.TimedAnimation (this, 0.0, 1.0, transition_duration, animation_target); animation.easing = Adw.Easing.EASE_IN_OUT_CUBIC; animation.done.connect ( () => { this.transition_animation = null; if (this.previous_page != null) { this.previous_page.child.set_child_visible (false); this.previous_page = null; } this.stop_resizing_window (); this.queue_resize (); }); this.transition_animation = animation; this.transition_animation.play (); } private void select_page (Ft.SizeStackPage page, bool transition = true) { if (page == this.current_page || this.in_destruction ()) { return; } // Store last focus/size for the current_page var contains_focus = false; var current_child = this.current_page?.child; if (current_child != null && this.transition_animation == null) { var focus = this.root?.get_focus (); var minimum_size = Gtk.Requisition (); var natural_size = Gtk.Requisition (); var last_width = -1; var last_height = -1; unowned Gtk.Widget? last_focus = null; if (focus != null && focus.is_ancestor (current_child)) { contains_focus = true; last_focus = focus; } else { last_focus = null; } last_width = current_child.get_width (); last_height = current_child.get_height (); if (last_width <= 0 || last_height <= 0) { current_child.get_preferred_size (out minimum_size, out natural_size); last_width = natural_size.width; last_height = natural_size.height; } this.current_page.last_focus = last_focus; this.current_page.last_width = last_width; this.current_page.last_height = last_height; } // Select page if (this.previous_page != null && this.previous_page != page) { this.previous_page.child.set_child_visible (false); this.previous_page = null; } this.previous_page = this.current_page; this.current_page = page; page.child.set_child_visible (true); if (contains_focus) { if (page.last_focus != null) { page.last_focus.grab_focus (); } else { page.child.child_focus (Gtk.DirectionType.TAB_FORWARD); } } if (this.current_page != null && this.previous_page != null) { this.start_transition (transition ? this.transition_duration : 0); } else { this.queue_resize (); } this.notify_property ("visible-child"); this.notify_property ("visible-child-name"); } private void remove_page (Ft.SizeStackPage page) { if (this.pages.index (page) < 0) { return; } page.child.unparent (); if (this.current_page == page) { this.current_page = null; } if (this.previous_page == page) { this.previous_page = null; } this.pages.remove (page); } private void add_page (Ft.SizeStackPage page) { if (this.pages.index (page) >= 0) { return; } var child = page.child; child.set_child_visible (false); child.set_parent (this); this.pages.append (page); if (this.current_page == null) { this.select_page (page, false); } } public new void add_child (Gtk.Builder builder, GLib.Object object, string? type) { if (object is Ft.SizeStackPage) { this.add_page ((Ft.SizeStackPage) object); } else if (object is Gtk.Widget) { this.add_page (new Ft.SizeStackPage ((Gtk.Widget) object)); } else { base.add_child (builder, object, type); } } public override void realize () { base.realize (); this.invalidate_window_default_size (); } public override void compute_expand_internal (out bool hexpand, out bool vexpand) { unowned var child = this.get_first_child (); hexpand = false; vexpand = false; while (child != null) { hexpand |= child.hexpand_set && child.hexpand; vexpand |= child.vexpand_set && child.vexpand; child = child.get_next_sibling (); } } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; if (this.current_page == null) { return; } var last_size = orientation == Gtk.Orientation.HORIZONTAL ? this.current_page.last_width : this.current_page.last_height; var minimum_for_size = 0; if (last_size <= 0 || this.transition_animation == null) { current_page.child.measure (get_opposite_orientation (orientation), -1, out minimum_for_size, null, null, null); current_page.child.measure (orientation, minimum_for_size, out minimum, out natural, null, null); } else { minimum = 0; natural = last_size; } if (this.previous_page != null) { var previous_size = orientation == Gtk.Orientation.HORIZONTAL ? this.previous_page.last_width : this.previous_page.last_height; var t = this.get_transition_progress (); natural = (int) GLib.Math.roundf ( Ft.lerpf ((float) previous_size, (float) natural, t)); } if (natural < minimum) { natural = minimum; } } public override void size_allocate (int width, int height, int baseline) { unowned var current_child = this.current_page?.child; unowned var previous_child = this.previous_page?.child; if (this.size_request_set) { this.size_request_set = false; // Lift minimum window size constraint at first opportunity. var window = this.get_root () as Gtk.Window; window?.set_size_request (-1, -1); } if (previous_child != null) { var previous_child_allocation = Gtk.Allocation () { x = 0, y = 0, width = int.max (this.previous_page.last_width, width), height = int.max (this.previous_page.last_height, height) }; // Do not transition width if there's a big discrepancy if (this.current_page.last_width > 2 * this.previous_page.last_width) { previous_child_allocation.width = this.previous_page.last_width; } previous_child.allocate_size (previous_child_allocation, baseline); } if (current_child != null) { var current_child_allocation = Gtk.Allocation () { x = 0, y = 0, width = width, height = height }; if (this.transition_animation != null) { current_child_allocation.width = int.max (this.current_page.last_width, width); current_child_allocation.height = int.max (this.current_page.last_height, height); } else { this.current_page.last_width = width; this.current_page.last_height = height; } current_child.allocate_size (current_child_allocation, baseline); } } public override void snapshot (Gtk.Snapshot snapshot) { unowned var current_child = this.current_page?.child; unowned var previous_child = this.previous_page?.child; if (current_child == null || !current_child.visible) { return; } if (this.transition_animation != null && previous_child != null && previous_child.visible) { var progress = (double) Math.powf ((float) this.get_transition_progress (), 1.5f); snapshot.push_cross_fade (progress); this.snapshot_child (previous_child, snapshot); snapshot.pop (); this.snapshot_child (current_child, snapshot); snapshot.pop (); } else { this.snapshot_child (current_child, snapshot); } } public override void dispose () { unowned var link = this.pages.first (); while (link != null) { this.remove_page (link.data); link = this.pages.first (); } this.pages = null; this.transition_animation = null; this.current_page = null; this.previous_page = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/main/window.ui000066400000000000000000000134411520625676500226600ustar00rootroot00000000000000
_Compact View window-compact-size-symbolic win.toggle-compact-size
_Preferences app.preferences _About app.about
_Quit app.quit
focustimerhq-FocusTimer-8581be2/src/ui/main/window.vala000066400000000000000000000350141520625676500231660ustar00rootroot00000000000000/* * Copyright (c) 2016-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public enum WindowSize { NORMAL = 0, COMPACT = 1; public static WindowSize from_string (string? name) { switch (name) { case "compact": return WindowSize.COMPACT; default: return WindowSize.NORMAL; } } public string to_string () { switch (this) { case NORMAL: return "normal"; case COMPACT: return "compact"; default: assert_not_reached (); } } } public enum WindowView { DEFAULT = 0, TIMER = 1, STATS = 2; public static WindowView from_string (string? view_name) { switch (view_name) { case "timer": return WindowView.TIMER; case "stats": return WindowView.STATS; default: return WindowView.DEFAULT; } } public string to_string () { switch (this) { case TIMER: return "timer"; case STATS: return "stats"; case DEFAULT: return ""; default: assert_not_reached (); } } } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/main/window.ui")] public class Window : Adw.ApplicationWindow, Gtk.Buildable { private const uint TOAST_DISMISS_TIMEOUT = 3; [CCode (notify = false)] public Ft.WindowSize size { get { return this._size; } set { if (this._size == value) { return; } this._size = value; this.size_stack.visible_child_name = value.to_string (); this.notify_property ("size"); } } [CCode (notify = false)] public Ft.WindowView view { get { return this._view; } set { if (this._view == value) { return; } this._view = value; var resolved_view = value; if (resolved_view == Ft.WindowView.DEFAULT) { resolved_view = this.get_default_view (); } this.view_stack.visible_child_name = resolved_view.to_string (); this.notify_property ("view"); } } [GtkChild] private unowned Ft.SizeStack size_stack; [GtkChild] private unowned Adw.ViewStack view_stack; [GtkChild] private unowned Adw.ToastOverlay toast_overlay; [GtkChild] private unowned Ft.TimerView timer_view; private Ft.WindowSize _size = Ft.WindowSize.NORMAL; private Ft.WindowView _view = Ft.WindowView.DEFAULT; private Ft.SessionManager? session_manager = null; private Ft.Timer? timer = null; private Ft.BackgroundManager? background_manager = null; private Peas.ExtensionSet? extensions = null; private static uint background_hold_id = 0U; construct { var settings = Ft.get_settings (); this.session_manager = Ft.SessionManager.get_default (); this.timer = session_manager.timer; this.timer.state_changed.connect (() => { this.update_timer_indicator (); }); this.insert_action_group ("session-manager", new Ft.SessionManagerActionGroup ()); this.insert_action_group ("timer", new Ft.TimerActionGroup ()); if (settings.get_boolean ("prefer-compact-size")) { this.size = Ft.WindowSize.COMPACT; } this.background_manager = new Ft.BackgroundManager (); this.notify["is-active"].connect (this.on_notify_is_active); this.notify["maximized"].connect (this.on_notify_maximized); this.update_title (); this.update_timer_indicator (); this.extensions = new Peas.ExtensionSet.with_properties ( Peas.Engine.get_default (), typeof (Ft.WindowExtension), {}, {}); this.extensions.extension_added.connect (this.on_extension_added); foreach_extension (this.extensions, (object) => { this.setup_extension (object); }); } private void update_title () { var page = this._size == Ft.WindowSize.NORMAL ? this.view_stack.get_page (this.view_stack.visible_child) : null; this.title = page != null ? page.title : _("Pomodoro"); } private void update_timer_indicator () { var timer_page = this.view_stack.get_page (this.timer_view); var timer = Ft.Timer.get_default (); timer_page.needs_attention = this.view_stack.visible_child != timer_page.child && timer.is_started (); } public Ft.WindowView get_default_view () ensures (result != Ft.WindowView.DEFAULT) { return Ft.WindowView.TIMER; } private async void close_to_background_internal () { var window_id = yield Ft.get_window_identifier (this); Ft.Window.background_hold_id = yield this.background_manager.hold (window_id); if (Ft.Window.background_hold_id != 0U) { this.close (); } else { this.minimize (); // fallback } } public void close_to_background () { this.close_to_background_internal.begin (); } /** * Keep the toast until window is focused. */ private void dismiss_toast_once_focused (Adw.Toast toast) { toast.timeout = 0; var state_flags_changed_id = this.state_flags_changed.connect ( (previous_state_flags) => { var is_backdrop = Gtk.StateFlags.BACKDROP in this.get_state_flags (); if (!is_backdrop) { toast.timeout = TOAST_DISMISS_TIMEOUT; this.toast_overlay.add_toast (toast); // necessary for updating the timeout } } ); toast.dismissed.connect (() => { if (state_flags_changed_id != 0) { this.disconnect (state_flags_changed_id); state_flags_changed_id = 0; } }); } /** * Monitor user activity and dismiss notification once user becomes active. */ private void dismiss_toast_once_user_becomes_active (Adw.Toast toast) { // toast.timeout = 0; // TODO } public void add_toast (owned Adw.Toast toast) { if (toast.timeout != 0 && this._size == Ft.WindowSize.NORMAL) { if (Gtk.StateFlags.BACKDROP in this.get_state_flags ()) { this.dismiss_toast_once_focused (toast); } else { this.dismiss_toast_once_user_becomes_active (toast); } } this.toast_overlay.add_toast (toast); } private void show_close_confirmation_dialog () { unowned var self = this; var dialog = new Adw.AlertDialog ( _("Keep timer running?"), _("You can keep it running in the background — notifications and keyboard shortcuts will still work.") ); dialog.prefer_wide_layout = true; dialog.add_response ("quit", _("Quit")); dialog.set_response_appearance ("quit", Adw.ResponseAppearance.DEFAULT); dialog.add_response ("run-in-background", _("Run in background")); dialog.set_response_appearance ("run-in-background", Adw.ResponseAppearance.SUGGESTED); dialog.set_default_response ("run-in-background"); dialog.set_close_response ("cancel"); dialog.response.connect ( (response) => { switch (response) { case "run-in-background": self.close_to_background (); break; case "quit": self.application.quit (); break; case "cancel": dialog.close (); break; default: assert_not_reached (); } }); dialog.present (self); } [GtkCallback] private void on_size_stack_visible_child_notify (GLib.Object object, GLib.ParamSpec pspec) { this.size = Ft.WindowSize.from_string (this.size_stack.visible_child_name); } [GtkCallback] private void on_view_stack_visible_child_notify (GLib.Object object, GLib.ParamSpec pspec) { var view = Ft.WindowView.from_string (this.view_stack.visible_child_name); this._view = this._view == Ft.WindowView.DEFAULT && this.get_default_view () == view ? Ft.WindowView.DEFAULT : view; this.update_title (); this.update_timer_indicator (); } [GtkCallback] private void on_gesture_click_pressed (Gtk.GestureClick gesture, int n_press, double x, double y) { var toggle_compact_size_action = this.lookup_action ("toggle-compact-size"); if (toggle_compact_size_action.enabled && gesture.get_current_button () == Gdk.BUTTON_PRIMARY && n_press == 2) { toggle_compact_size_action.activate (null); gesture.set_state (Gtk.EventSequenceState.CLAIMED); } } [GtkCallback] private bool on_close_request () { if (this.background_manager.active) { return false; } if (this.timer.is_running ()) { this.show_close_confirmation_dialog (); return true; } else { this.application.quit (); return false; } } private void on_notify_is_active (GLib.Object object, GLib.ParamSpec pspec) { if (this.is_active && Ft.Window.background_hold_id != 0U) { this.background_manager.release (Ft.Window.background_hold_id); Ft.Window.background_hold_id = 0U; } } private void on_notify_maximized (GLib.Object object, GLib.ParamSpec pspec) { var can_change_size = !this.maximized; var compact_size_action = (GLib.SimpleAction) this.lookup_action ("compact-size"); compact_size_action.set_enabled (can_change_size); var toggle_compact_size_action = (GLib.SimpleAction) this.lookup_action ("toggle-compact-size"); toggle_compact_size_action.set_enabled (can_change_size); } private void on_compact_size_activate (GLib.SimpleAction action, GLib.Variant? parameter) { this.size = Ft.WindowSize.COMPACT; } private void on_normal_size_activate (GLib.SimpleAction action, GLib.Variant? parameter) { this.size = Ft.WindowSize.NORMAL; this.view = Ft.WindowView.TIMER; } private void on_toggle_compact_size_activate (GLib.SimpleAction action, GLib.Variant? parameter) { if (this.size == Ft.WindowSize.NORMAL) { this.lookup_action ("compact-size").activate (null); } else { this.lookup_action ("normal-size").activate (null); } } private void on_extension_added (Peas.ExtensionSet extension_set, Peas.PluginInfo plugin_info, GLib.Object object) { this.setup_extension (object); } private void setup_extension (GLib.Object object) { var extension = (Ft.WindowExtension) object; extension.window = this; } private void setup_actions () { var action_map = (GLib.ActionMap) this; var compact_size_action = new GLib.SimpleAction ("compact-size", null); compact_size_action.activate.connect (this.on_compact_size_activate); action_map.add_action (compact_size_action); var normal_size_action = new GLib.SimpleAction ("normal-size", null); normal_size_action.activate.connect (this.on_normal_size_activate); action_map.add_action (normal_size_action); var toggle_compact_size_action = new GLib.SimpleAction ("toggle-compact-size", null); toggle_compact_size_action.activate.connect (this.on_toggle_compact_size_activate); action_map.add_action (toggle_compact_size_action); } public void parser_finished (Gtk.Builder builder) { this.setup_actions (); } public override void dispose () { this.extensions = null; this.background_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/overlays/000077500000000000000000000000001520625676500217275ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/overlays/lightbox.ui000066400000000000000000000017471520625676500241170ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/overlays/lightbox.vala000066400000000000000000000664201520625676500244240ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { /** * A primary monitor used to be defined in X11. Nowadays a primary monitor might be chosen * by the compositor. It's unlikely that we will integrate properly with all compositors. */ private unowned Gdk.Monitor? get_primary_monitor (Gdk.Display? display = null) { if (display == null) { display = Gdk.Display.get_default (); } unowned Gdk.Monitor? primary_monitor = null; var primary_monitor_area = 0; var monitors = display?.get_monitors (); if (monitors == null) { return null; } for (var index = 0; index < monitors.get_n_items (); index++) { var monitor = (Gdk.Monitor?) monitors.get_item (index); var monitor_area = monitor.valid ? monitor.width_mm * monitor.height_mm : 0; if (monitor_area > primary_monitor_area) { primary_monitor = monitor; primary_monitor_area = monitor_area; } } // Fall back to using the first valid monitor. if (primary_monitor == null) { for (var index = 0; index < monitors.get_n_items (); index++) { var monitor = (Gdk.Monitor?) monitors.get_item (index); if (monitor.valid) { primary_monitor = monitor; break; } } } return primary_monitor; } private Gdk.Rectangle get_display_geometry (Gdk.Display? display = null) { if (display == null) { display = Gdk.Display.get_default (); } var display_geometry = Gdk.Rectangle () { x = 0, y = 0, width = 0, height = 0, }; var monitors = display?.get_monitors (); if (monitors == null) { return display_geometry; } for (var index = 0; index < monitors.get_n_items (); index++) { var monitor = (Gdk.Monitor?) monitors.get_item (index); if (monitor == null) { continue; } if (index == 0) { display_geometry = monitor.get_geometry (); } else { display_geometry.union (monitor.get_geometry (), out display_geometry); } } return display_geometry; } /** * Layout manager that positions window contents within the bounds of a monitor. * * It's necessary for handling `Gdk.FullscreenMode.ALL_MONITORS`. */ private class MonitorConstrainedLayoutManager : Gtk.LayoutManager { public Gdk.Monitor? monitor { get { return this._monitor; } set { this._monitor = value; this.layout_changed (); } } private Gdk.Monitor? _monitor = null; public MonitorConstrainedLayoutManager (Gdk.Monitor? monitor = null) { this.monitor = monitor; } public override Gtk.SizeRequestMode get_request_mode (Gtk.Widget widget) { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Widget widget, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { if (this._monitor != null) { minimum = natural = orientation == Gtk.Orientation.HORIZONTAL ? this._monitor.geometry.width : this._monitor.geometry.height; } else { minimum = natural = 0; } minimum_baseline = -1; natural_baseline = -1; } public override void allocate (Gtk.Widget widget, int width, int height, int baseline) { var allocation = Gtk.Allocation () { x = 0, y = 0, width = width, height = height }; if (this._monitor != null) { var monitor_geometry = this._monitor.get_geometry (); allocation.x = monitor_geometry.x; allocation.y = monitor_geometry.y; allocation.width = monitor_geometry.width; allocation.height = monitor_geometry.height; } var child = widget.get_first_child (); while (child != null) { if (!child.should_layout ()) { continue; } child.allocate_size (allocation, -1); child = child.get_next_sibling (); } } } private errordomain LightboxError { NO_MONITORS, } /** * Helper class for handling overlays on multiple screens. */ public class LightboxGroup : GLib.InitiallyUnowned { private GLib.Type lightbox_type; private GLib.GenericSet? lightboxes = null; private Gtk.WindowGroup? window_group = null; private GLib.ListModel? monitors = null; private ulong monitors_changed_id = 0; private uint updating_count = 0; private uint update_idle_id = 0; private GLib.SourceFunc? open_callback = null; public LightboxGroup (GLib.Type lightbox_type) { this.lightbox_type = lightbox_type; this.lightboxes = new GLib.GenericSet (GLib.direct_hash, GLib.direct_equal); } private unowned Ft.Lightbox? get_first_lightbox () { unowned Ft.Lightbox? first_lightbox = null; this.lightboxes.@foreach ( (lightbox) => { if (lightbox != null) { first_lightbox = lightbox; } }); return first_lightbox; } private Ft.Lightbox create_lightbox (Gdk.Monitor? monitor_request) { var monitor_request_value = GLib.Value (typeof (Gdk.Monitor?)); monitor_request_value.set_object (monitor_request); var show_contents_value = GLib.Value (typeof (bool)); show_contents_value.set_boolean (false); var lightbox = (Ft.Lightbox) GLib.Object.new_with_properties ( this.lightbox_type, { "monitor-request", "show-contents" }, { monitor_request_value, show_contents_value }); lightbox.notify["monitor"].connect (this.on_lightbox_notify_monitor); lightbox.close_request.connect (this.on_lightbox_close_request); var lightbox_widget = (Gtk.Widget) lightbox; lightbox_widget.unrealize.connect (this.on_lightbox_unrealize); this.window_group.add_window (lightbox); this.lightboxes.add (lightbox); // Note that the new window may steal focus from the primary window. lightbox.present (); return lightbox; } private void destroy_lightbox (Ft.Lightbox lightbox) { lightbox.notify["monitor"].disconnect (this.on_lightbox_notify_monitor); lightbox.close_request.disconnect (this.on_lightbox_close_request); var lightbox_widget = (Gtk.Widget) lightbox; lightbox_widget.unrealize.disconnect (this.on_lightbox_unrealize); this.lightboxes.remove (lightbox); lightbox.close (); } private void update_lightboxes () { if (this.lightboxes.length == 0) { this.create_lightbox (null); return; } if (this.lightboxes.length == 1) { var lightbox = this.get_first_lightbox (); if (lightbox.all_monitors_mode) { return; } } var paired_monitors = new GLib.GenericSet ( GLib.direct_hash, GLib.direct_equal); var paired_lightboxes = new GLib.GenericSet ( GLib.direct_hash, GLib.direct_equal); this.lightboxes.@foreach ( (lightbox) => { var lightbox_monitor = lightbox.monitor; if (lightbox_monitor != null) { lightbox.monitor_request = lightbox_monitor; } if (lightbox_monitor != null && !paired_monitors.contains (lightbox_monitor)) { paired_monitors.add (lightbox_monitor); paired_lightboxes.add (lightbox); } }); this.lightboxes.@foreach ( (lightbox) => { if (lightbox.monitor_request != null && !lightbox.get_mapped () && !paired_monitors.contains (lightbox.monitor_request) && !paired_lightboxes.contains (lightbox)) { paired_monitors.add (lightbox.monitor_request); paired_lightboxes.add (lightbox); } }); this.lightboxes.get_values ().@foreach ( (lightbox) => { if (!paired_lightboxes.contains (lightbox)) { this.destroy_lightbox (lightbox); } }); // Spawn windows on unused monitors. for (var index = 0U; index < this.monitors.get_n_items (); index++) { var monitor = (Gdk.Monitor?) this.monitors.get_item (index); if (monitor != null && monitor.valid && !paired_monitors.contains (monitor)) { var lightbox = this.create_lightbox (monitor); paired_monitors.add (monitor); paired_lightboxes.add (lightbox); } } if (paired_monitors.length != paired_lightboxes.length) { GLib.warning ("Mismatch between number of windows (%u) and monitors (%u).", paired_lightboxes.length, paired_monitors.length); } } private void update () { if (this.updating_count > 0 || this.monitors == null) { return; } if (this.update_idle_id != 0) { GLib.Source.remove (this.update_idle_id); this.update_idle_id = 0; } this.updating_count++; this.update_lightboxes (); this.updating_count--; } private void queue_update () { if (this.update_idle_id != 0) { return; } this.update_idle_id = GLib.Idle.add ( () => { this.update_idle_id = 0; this.update (); return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.update_idle_id, "Ft.LightboxGroup.update"); } private void on_monitors_changed (GLib.ListModel model, uint position, uint removed, uint added) { // Lightboxes would try to allocate with previous size before monitors changed. this.lightboxes.@foreach ( (lightbox) => { lightbox.queue_resize (); }); this.queue_update (); } private void on_lightbox_notify_monitor (GLib.Object object, GLib.ParamSpec pspec) { this.queue_update (); } private bool on_lightbox_close_request (Gtk.Window window) { if (this.updating_count == 0) { this.close (); } return false; } private void on_lightbox_unrealize (Gtk.Widget widget) { this.destroy_lightbox ((Ft.Lightbox) widget); } public async void open (GLib.Cancellable? cancellable = null) throws GLib.Error requires (this.open_callback == null) { if (this.monitors == null) { this.monitors = Gdk.Display.get_default ()?.get_monitors (); } if (this.monitors == null) { throw new Ft.LightboxError.NO_MONITORS ("Could not list monitors."); } if (this.monitors_changed_id == 0) { this.monitors_changed_id = this.monitors.items_changed.connect (this.on_monitors_changed); } this.window_group = new Gtk.WindowGroup (); this.update (); this.open_callback = this.open.callback; if (cancellable != null) { cancellable.cancelled.connect (this.close); } yield; this.open_callback = null; if (this.monitors_changed_id != 0) { this.monitors.disconnect (this.monitors_changed_id); this.monitors_changed_id = 0; } this.lightboxes.get_values ().@foreach ( (lightbox) => { lightbox.close (); }); } private void close () { if (this.open_callback != null) { this.open_callback (); } } public override void dispose () { if (this.update_idle_id != 0) { GLib.Source.remove (this.update_idle_id); this.update_idle_id = 0; } if (this.monitors_changed_id != 0) { this.monitors.disconnect (this.monitors_changed_id); this.monitors_changed_id = 0; } if (this.lightboxes != null) { this.lightboxes.remove_all (); this.lightboxes = null; } this.window_group = null; this.monitors = null; base.dispose (); } } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/overlays/lightbox.ui")] public class Lightbox : Gtk.Window, Gtk.Buildable { public Gdk.Monitor? monitor_request { get { return this._monitor_request; } set { this._monitor_request = value; } } public Gdk.Monitor? monitor { get { return this._monitor; } } public Gtk.Widget? contents { get { return this.container.child; } set { var previous_child = this.container.child; if (previous_child != null) { previous_child.remove_css_class ("contents"); } this.container.child = value; if (value != null) { value.add_css_class ("contents"); } } } public bool all_monitors_mode { get { return this._all_monitors_mode; } } public bool show_contents { get { return this.container.visible; } set { this.container.visible = value; } } [GtkChild] private unowned Adw.Bin container; private Gdk.Monitor? _monitor_request = null; private Gdk.Monitor? _monitor = null; private GLib.GenericSet monitors = null; private bool _all_monitors_mode = false; private bool closing = false; private static uint next_id = 0; internal uint id = 0; static construct { set_css_name ("lightbox"); set_auto_startup_notification (false); } construct { this.id = next_id; next_id++; this.monitors = new GLib.GenericSet (GLib.direct_hash, GLib.direct_equal); this.notify["fullscreened"].connect (this.on_notify_fullscreened); } private void update_monitor () { unowned Gdk.Monitor? monitor; var layout_manager = (Ft.MonitorConstrainedLayoutManager) this.container.layout_manager; var display = this.get_display (); var surface = this.get_surface (); var surface_monitor = surface != null ? display?.get_monitor_at_surface (surface) : null; var primary_monitor = get_primary_monitor (display); if (this._all_monitors_mode) { if (this.monitors.contains (primary_monitor)) { monitor = primary_monitor; layout_manager.monitor = primary_monitor; } else { monitor = surface_monitor; layout_manager.monitor = surface_monitor; } this.show_contents = true; } else { monitor = surface_monitor; layout_manager.monitor = null; this.show_contents = monitor == primary_monitor; } if (this._monitor != monitor) { if (this._monitor != null) { this._monitor.notify["valid"].disconnect (this.on_monitor_notify_valid); } this._monitor = monitor; if (this._monitor != null) { this._monitor.notify["valid"].connect (this.on_monitor_notify_valid); } this.notify_property ("monitor"); } } private void remove_invalid_monitors () { this.monitors.get_values ().@foreach ( (monitor) => { if (!monitor.valid) { this.monitors.remove (monitor); } }); } private void on_notify_fullscreened (GLib.Object object, GLib.ParamSpec pspec) { if (this.closing) { return; } if (!this.fullscreened) { GLib.debug ("Failed to make lightbox fullscreen. Closing..."); this.close (); } } private void on_monitor_notify_valid (GLib.Object object, GLib.ParamSpec pspec) { if (!monitor.valid) { this.queue_allocate(); } } private void on_enter_monitor (Gdk.Monitor? monitor) { if (monitor != null) { this.monitors.add (monitor); this.queue_allocate(); } this.remove_invalid_monitors (); } private void on_leave_monitor (Gdk.Monitor? monitor) { if (monitor != null) { this.monitors.remove (monitor); this.queue_allocate(); } this.remove_invalid_monitors (); } [GtkCallback] private bool on_key_pressed (Gtk.EventControllerKey event_controller, uint keyval, uint keycode, Gdk.ModifierType state) { switch (keyval) { case Gdk.Key.Escape: this.close (); return true; } return false; } private bool resolve_all_monitors_mode (int width, int height) { if (this._monitor_request != null) { return false; } var display = this.get_display (); var display_geometry = get_display_geometry (display); var monitors = display.get_monitors (); var monitors_count = monitors.get_n_items (); if (monitors_count == 0) { return false; } if (monitors_count == 1) { return true; } for (var index = 0; index < monitors.get_n_items (); index++) { var monitor = (Gdk.Monitor?) monitors.get_item (index); if (monitor.valid && !this.monitors.contains (monitor)) { return false; } } return width >= display_geometry.width && height >= display_geometry.height; } /** * We don't have access to compositor coordinates. */ private void update_all_monitors_mode (int width, int height) { var all_monitors_mode = this.resolve_all_monitors_mode (width, height); if (this._all_monitors_mode != all_monitors_mode) { this._all_monitors_mode = all_monitors_mode; this.notify_property ("all-monitors-mode"); } } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var monitor = this._monitor != null ? this._monitor : this._monitor_request; if (this._monitor_request == null && this.monitors.length > 1) { var display_geometry = get_display_geometry (this.get_display ()); minimum = natural = orientation == Gtk.Orientation.HORIZONTAL ? display_geometry.width : display_geometry.height; } else if (monitor != null && this.monitors.length == 1) { var monitor_geometry = monitor.geometry; minimum = natural = orientation == Gtk.Orientation.HORIZONTAL ? monitor_geometry.width : monitor_geometry.height; } else { base.measure (orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { if (this.monitors.length > 0) { this.update_all_monitors_mode (width, height); this.update_monitor (); // Remind the compositor to do full-screen on all monitors. var toplevel = (Gdk.Toplevel) this.get_surface (); if (this._all_monitors_mode) { toplevel.fullscreen_mode = Gdk.FullscreenMode.ALL_MONITORS; this.fullscreen (); } else if (this._monitor != null) { toplevel.fullscreen_mode = Gdk.FullscreenMode.CURRENT_MONITOR; this.fullscreen_on_monitor (this._monitor); } else if (this._monitor_request != null) { toplevel.fullscreen_mode = Gdk.FullscreenMode.CURRENT_MONITOR; this.fullscreen_on_monitor (this._monitor_request); } else { toplevel.fullscreen_mode = Gdk.FullscreenMode.CURRENT_MONITOR; this.fullscreen (); } } base.size_allocate (width, height, baseline); } // public override void realize () // { // base.realize (); // // var surface = this.get_surface (); // var display = surface?.get_display (); // // #if HAVE_GDK_X11 // if (display is Gdk.X11.Display) // { // var x11_surface = surface as Gdk.X11.Surface; // // x11_surface.set_skip_pager_hint (true); // x11_surface.set_skip_taskbar_hint (true); // } // #endif // } public override void map () { var toplevel = (Gdk.Toplevel) this.get_surface (); toplevel.enter_monitor.connect (this.on_enter_monitor); toplevel.leave_monitor.connect (this.on_leave_monitor); if (this._monitor_request == null) { // Request opening window on all monitors. If it fails, open additional windows later. toplevel.fullscreen_mode = Gdk.FullscreenMode.ALL_MONITORS; this.fullscreen (); } else { this.fullscreen_on_monitor (this._monitor_request); } base.map (); } public override void unmap () { var toplevel = (Gdk.Toplevel) this.get_surface (); toplevel.enter_monitor.disconnect (this.on_enter_monitor); toplevel.leave_monitor.disconnect (this.on_leave_monitor); base.unmap (); } public override void dispose () { if (this._monitor != null) { this._monitor.notify["valid"].disconnect (this.on_monitor_notify_valid); } if (this.monitors != null) { this.monitors.remove_all (); this.monitors = null; } this.notify["fullscreened"].disconnect (this.on_notify_fullscreened); this._monitor_request = null; this._monitor = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/overlays/screen-overlay.ui000066400000000000000000000056231520625676500252320ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/overlays/screen-overlay.vala000066400000000000000000000025371520625676500255410ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/overlays/screen-overlay.ui")] public class ScreenOverlay : Ft.Lightbox { [GtkChild] private unowned Gtk.Button lock_screen_button; private Ft.LockScreen? lock_screen; construct { this.lock_screen = new Ft.LockScreen (); this.lock_screen.bind_property ("enabled", this.lock_screen_button, "visible", GLib.BindingFlags.SYNC_CREATE); } [GtkCallback] private void on_lock_screen_button_clicked (Gtk.Button button) { this.lock_screen.activate (); } [GtkCallback] private void on_close_button_clicked (Gtk.Button button) { this.close (); } // public override void map () // { // base.map (); // // // TODO: Reset user idle-time to delay the screen-saver. // // Ft.wake_up_screen (); // } public override void dispose () { this.lock_screen = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/000077500000000000000000000000001520625676500223645ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/appearance/000077500000000000000000000000001520625676500244635ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/appearance/preferences-panel-appearance.ui000066400000000000000000000024671520625676500325260ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/appearance/preferences-panel-appearance.vala000066400000000000000000000025371520625676500330320ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/appearance/preferences-panel-appearance.ui")] public class PreferencesPanelAppearance : Ft.PreferencesPanel { [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Adw.SwitchRow dark_theme_switchrow; [GtkChild] private unowned Adw.SwitchRow compact_view_switchrow; private GLib.Settings? settings = null; construct { this.settings = Ft.get_settings (); this.settings.bind ("dark-theme", this.dark_theme_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("prefer-compact-size", this.compact_view_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/000077500000000000000000000000001520625676500245445ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/000077500000000000000000000000001520625676500260215ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/action-edit-window.ui000066400000000000000000000356321520625676500320760ustar00rootroot00000000000000
$state app.quit Started app.quit Paused app.quit Finished app.quit
focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/action-edit-window.vala000066400000000000000000000452471520625676500324070ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { private Ft.Expression? ensure_operation (Ft.Expression? expression) { return (expression is Ft.Operation) ? expression : new Ft.Operation (Ft.Operator.AND, expression); } private class EventRow : Adw.ActionRow { public string event_name { get; construct; } construct { var remove_button = new Gtk.Button (); remove_button.icon_name = "window-close-symbolic"; remove_button.valign = Gtk.Align.CENTER; remove_button.add_css_class ("flat"); remove_button.clicked.connect (() => this.request_remove ()); this.add_suffix (remove_button); } public EventRow (string event_name, string title, string subtitle) { GLib.Object ( event_name: event_name, title: title, subtitle: subtitle ); } public signal void request_remove (); } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/action-edit-window.ui")] public class ActionEditWindow : Adw.Window { public string? action_uuid { get; construct; } public bool creating { get; construct; } public Ft.ActionTrigger trigger { get { return this.event_radio.active ? Ft.ActionTrigger.EVENT : Ft.ActionTrigger.CONDITION; } set { this.event_radio.active = value == Ft.ActionTrigger.EVENT; this.condition_radio.active = value == Ft.ActionTrigger.CONDITION; } } public bool enabled { get { return this.enabled_switch.active; } set { this.enabled_switch.active = value; } } public string display_name { get { return this.display_name_entryrow.text; } set { this.display_name_entryrow.text = value; } } public string command_line { get { return this.command_entryrow.text; } set { this.command_entryrow.text = value; } } public string enter_command_line { get { return this.enter_command_entryrow.text; } set { this.enter_command_entryrow.text = value; } } public string exit_command_line { get { return this.exit_command_entryrow.text; } set { this.exit_command_entryrow.text = value; } } public string working_directory { get { return this.working_directory_entryrow.text; } set { this.working_directory_entryrow.text = value; } } public bool use_subshell { get { return this.use_subshell_switchrow.active; } set { this.use_subshell_switchrow.active = value; } } public bool pass_input { get { return this.pass_input_switchrow.active; } set { this.pass_input_switchrow.active = value; } } public bool wait_for_completion { get { return this.wait_for_completion_switchrow.active; } set { this.wait_for_completion_switchrow.active = value; } } [GtkChild] private unowned Gtk.Button save_button; [GtkChild] private unowned Gtk.Switch enabled_switch; [GtkChild] private unowned Adw.EntryRow display_name_entryrow; [GtkChild] private unowned Gtk.CheckButton event_radio; [GtkChild] private unowned Gtk.CheckButton condition_radio; [GtkChild] private unowned Adw.PreferencesGroup events_group; [GtkChild] private unowned Ft.ConditionGroupWidget condition_group_widget; [GtkChild] private unowned Gtk.MenuButton add_event_button; [GtkChild] private unowned Gtk.ToggleButton event_condition_button; [GtkChild] private unowned Adw.PreferencesGroup event_condition_group; [GtkChild] private unowned Ft.ConditionGroupWidget event_condition_group_widget; [GtkChild] private unowned Adw.EntryRow working_directory_entryrow; [GtkChild] private unowned Adw.SwitchRow use_subshell_switchrow; [GtkChild] private unowned Adw.SwitchRow pass_input_switchrow; [GtkChild] private unowned Adw.SwitchRow wait_for_completion_switchrow; [GtkChild] private unowned Ft.CommandEntryRow command_entryrow; [GtkChild] private unowned Ft.CommandEntryRow enter_command_entryrow; [GtkChild] private unowned Ft.CommandEntryRow exit_command_entryrow; [GtkChild] private unowned Adw.PreferencesGroup buttons_group; private Ft.ActionManager? action_manager = null; private Ft.EventProducer? event_producer = null; private unowned Gtk.ListBox? events_listbox = null; public ActionEditWindow (string? action_uuid = null) { GLib.Object ( action_uuid: action_uuid, creating: action_uuid == null ); } static construct { install_action ("add-event", "s", (Gtk.WidgetActionActivateFunc) on_add_event); } construct { var text_widget = get_child_by_buildable_id (this.display_name_entryrow, "text") as Gtk.Text; var events_listbox = get_child_by_buildable_id (this.events_group, "listbox") as Gtk.ListBox; if (text_widget != null) { text_widget.max_length = 50; } else { GLib.warning ("Could not find text widget."); } if (events_listbox != null) { var placeholder_label = new Gtk.Label (_("No events specified yet.")); placeholder_label.height_request = 50; placeholder_label.add_css_class ("dim-label"); events_listbox.set_placeholder (placeholder_label); } else { GLib.warning ("Could not find events listbox."); } this.action_manager = new Ft.ActionManager (); this.event_producer = new Ft.EventProducer (); // TODO: get from action manager this.add_event_button.menu_model = this.create_add_event_menu (); this.events_listbox = events_listbox; if (this.creating) { this.title = _("Add Custom Action"); this.save_button.label = _("_Add"); this.buttons_group.visible = false; } this.populate (); } private void update_event_condition_visible () { this.event_condition_group.visible = this.event_radio.active && this.event_condition_button.active; } private void populate () { var action = this.action_uuid != null ? this.action_manager.model.lookup (this.action_uuid) : null; if (action == null) { action = new Ft.EventAction (this.action_uuid); } this.enabled = action.enabled; this.display_name = action.display_name; if (action is Ft.EventAction) { var event_action = (Ft.EventAction) action; var condition = event_action.condition; var command = event_action.command; var row = this.events_group.get_first_child (); while (row != null) { var next_sibling = row.get_next_sibling (); if (row is Ft.EventRow) { this.events_group.remove (row); } row = next_sibling; } foreach (var event_name in event_action.event_names) { row = this.create_event_row (event_name); if (row != null) { this.events_group.add (row); } } this.trigger = Ft.ActionTrigger.EVENT; this.event_condition_group_widget.expression = ensure_operation (condition); this.event_condition_button.active = condition != null; this.wait_for_completion = event_action.wait_for_completion; if (command != null) { this.command_line = command.line; this.working_directory = command.working_directory; this.use_subshell = command.use_subshell; this.pass_input = command.pass_input; } } if (action is Ft.ConditionAction) { var condition_action = (Ft.ConditionAction) action; var condition = action.condition; var enter_command = condition_action.enter_command; var exit_command = condition_action.exit_command; var any_command = enter_command != null ? enter_command : exit_command; this.trigger = Ft.ActionTrigger.CONDITION; this.condition_group_widget.expression = ensure_operation (condition); if (enter_command != null) { this.enter_command_line = enter_command.line; } if (exit_command != null) { this.exit_command_line = exit_command.line; } if (any_command != null) { this.working_directory = any_command.working_directory; this.use_subshell = any_command.use_subshell; this.pass_input = any_command.pass_input; } } this.update_event_condition_visible (); } private void save_action () { var action = this.action_manager.model.create_action (this.action_uuid, this.trigger); action.enabled = this.enabled; action.display_name = this.display_name; if (action is Ft.EventAction) { var event_action = (Ft.EventAction) action; event_action.event_names = this.get_event_names (); event_action.condition = event_condition_button.active ? this.event_condition_group_widget.expression : null; event_action.wait_for_completion = this.wait_for_completion; event_action.command = new Ft.Command (this.command_line); event_action.command.working_directory = this.working_directory; event_action.command.use_subshell = this.use_subshell; event_action.command.pass_input = this.pass_input; } if (action is Ft.ConditionAction) { var condition_action = (Ft.ConditionAction) action; condition_action.condition = this.condition_group_widget.expression; condition_action.enter_command = new Ft.Command (this.enter_command_line); condition_action.enter_command.working_directory = this.working_directory; condition_action.enter_command.use_subshell = this.use_subshell; condition_action.enter_command.pass_input = this.pass_input; condition_action.exit_command = new Ft.Command (this.exit_command_line); condition_action.exit_command.working_directory = this.working_directory; condition_action.exit_command.use_subshell = this.use_subshell; condition_action.exit_command.pass_input = this.pass_input; } this.action_manager.model.save_action (action); } private void delete_action () { if (this.action_uuid != null) { this.action_manager.model.delete_action (this.action_uuid); } } private GLib.Menu create_add_event_menu () { var menu = new GLib.Menu (); var sections = new GLib.HashTable ( GLib.direct_hash, GLib.direct_equal); foreach (var event_spec in this.event_producer.list_events ()) { var section = sections.lookup (event_spec.category); if (section == null) { var new_section = new GLib.Menu (); sections.insert (event_spec.category, new_section); section = new_section; } var section_item = new GLib.MenuItem (event_spec.display_name, null); section_item.set_action_and_target_value ( "add-event", new GLib.Variant.string (event_spec.name)); section.append_item (section_item); } Ft.EventCategory.@foreach ( (category) => { var section = sections.lookup (category); if (section != null) { menu.append_section (category.get_label (), section); } }); return menu; } private void open_working_directory_chooser () { var working_directory = this.working_directory != "" ? this.working_directory : GLib.Environment.get_home_dir (); var directory_filter = new Gtk.FileFilter (); directory_filter.add_mime_type ("inode/directory"); var file_dialog = new Gtk.FileDialog (); file_dialog.title = _("Select Working Directory"); file_dialog.modal = true; file_dialog.accept_label = _("_Select"); file_dialog.default_filter = directory_filter; file_dialog.initial_file = File.new_for_path (working_directory); file_dialog.select_folder.begin ( this, null, (obj, res) => { GLib.File? file = null; try { file = file_dialog.select_folder.end (res); } catch (GLib.Error error) { return; } if (file != null) { this.working_directory = file.get_path (); } }); } [GtkCallback] public void on_cancel_button_clicked () { this.close (); } [GtkCallback] public void on_save_button_clicked () { this.save_action (); this.close (); } [GtkCallback] public void on_delete_button_clicked () { this.delete_action (); this.close (); } [GtkCallback] public void on_trigger_radio_toggled (Gtk.CheckButton radio) { this.update_event_condition_visible (); } [GtkCallback] private void on_event_condition_button_toggled () { this.update_event_condition_visible (); } [GtkCallback] private void on_event_condition_request_remove () { this.event_condition_button.active = false; this.update_event_condition_visible (); } [GtkCallback] private void on_working_directory_button_clicked () { this.open_working_directory_chooser (); } [GtkCallback] private bool on_key_pressed (Gtk.EventControllerKey event_controller, uint keyval, uint keycode, Gdk.ModifierType state) { switch (keyval) { case Gdk.Key.Escape: this.close (); return true; } return false; } private string[] get_event_names () { unowned var listbox = (Gtk.ListBox) this.events_listbox; unowned var child = listbox.get_first_child (); string[] event_names = {}; while (child != null) { if (child is EventRow) { var row = (EventRow) child; event_names += row.event_name; } child = child.get_next_sibling (); } return event_names; } private Adw.ActionRow? create_event_row (string event_name) { var event_spec = this.event_producer.find_event (event_name); if (event_spec == null) { GLib.warning ("Could not find event '%s'.", event_name); return null; } var row = new EventRow (event_name, event_spec.display_name, event_spec.description); row.request_remove.connect (this.events_group.remove); return row; } private void on_add_event (string action_name, GLib.Variant? parameter) { if (parameter == null) { return; } var row = this.create_event_row (parameter.get_string ()); if (row != null) { this.events_group.add (row); } } public override bool close_request () { var cancelled = base.close_request (); if (!cancelled && this.creating) { this.delete_action (); } return cancelled; } public override void dispose () { this.action_manager = null; this.event_producer = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/action-listboxrow.ui000066400000000000000000000044341520625676500320540ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/action-listboxrow.vala000066400000000000000000000100411520625676500323510ustar00rootroot00000000000000/* * Copyright (c) 2016, 2024 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/action-listboxrow.ui")] public class ActionListBoxRow : Gtk.ListBoxRow { public Ft.Action action { get { return this._action; } set { if (this._action == value) { return; } if (this.enabled_switch_binding != null) { this.enabled_switch_binding.unbind (); this.enabled_switch_binding = null; } this._action = value; this.enabled_switch_binding = this._action.bind_property ( "enabled", this.enabled_switch, "active", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); this.update_display_name (); } } public uint sort_order { get; set; default = 0U; } [GtkChild] private unowned Gtk.Label display_name_label; [GtkChild] private unowned Gtk.Switch enabled_switch; private Ft.Action? _action = null; private unowned GLib.Binding? enabled_switch_binding = null; private double drag_x; private double drag_y; private Gtk.ListBox? drag_widget = null; public ActionListBoxRow (Ft.Action action) { GLib.Object ( action: action ); } /** * Assume that action-list will set a new `action` here after it gets saved. */ private void update_display_name () { if (this._action != null) { this.display_name_label.label = this._action.display_name != "" ? this._action.display_name : _("Untitled action"); } } [GtkCallback] private Gdk.ContentProvider? on_drag_prepare (double x, double y) { this.drag_x = x; this.drag_y = y; var drag_value = GLib.Value (typeof (Ft.ActionListBoxRow)); drag_value.set_object (this); return new Gdk.ContentProvider.for_value (drag_value); } [GtkCallback] private void on_drag_begin (Gdk.Drag drag) { var row = new Ft.ActionListBoxRow (this._action); this.drag_widget = new Gtk.ListBox (); this.drag_widget.set_size_request (this.get_width (), this.get_height ()); this.drag_widget.append (row); this.drag_widget.drag_highlight_row (row); #if VALA_0_56_19 var drag_icon = new Gtk.DragIcon.get_for_drag (drag); #else var drag_icon = (Gtk.DragIcon) Gtk.DragIcon.get_for_drag (drag); #endif drag_icon.child = this.drag_widget; drag.set_hotspot ((int) this.drag_x, (int) this.drag_y); } [GtkCallback] private bool on_drop (GLib.Value value, double x, double y) { if (!value.holds (typeof (Ft.ActionListBoxRow))) { return false; } var source = (Ft.ActionListBoxRow) value.get_object (); source.move_row (this); return true; } [HasEmitter] public signal void move_row (Ft.ActionListBoxRow destination_row); public override void dispose () { if (this.enabled_switch_binding != null) { this.enabled_switch_binding.unbind (); this.enabled_switch_binding = null; } this._action = null; this.drag_widget = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/command-entryrow.ui000066400000000000000000000014301520625676500316630ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/command-entryrow.vala000066400000000000000000000112671520625676500322020ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/command-entryrow.ui")] public class CommandEntryRow : Adw.EntryRow { public bool use_subshell { get { return this._use_subshell; } set { this._use_subshell = value; this.update (); } } private bool _use_subshell = false; private Ft.Command _command = null; private unowned Gtk.Image edit_icon = null; private unowned Gtk.Text? text_widget = null; private bool text_changed = false; construct { this.text_widget = get_child_by_buildable_id (this.child, "text") as Gtk.Text; this.edit_icon = get_child_by_buildable_id (this.child, "edit_icon") as Gtk.Image; if (this.text_widget != null) { this.text_widget.max_length = 500; this.text_widget.state_flags_changed.connect (this.on_text_widget_state_flags_changed); this.text_widget.changed.connect (this.on_text_widget_changed); } else { GLib.warning ("Could not find text widget."); } if (this.edit_icon == null) { GLib.warning ("Could not find edit icon."); } } private void insert_at_cursor (string text) { var initial_position = this.cursor_position; var position = initial_position; this.do_insert_text (text, text.length, ref position); if (position != initial_position) { this.set_position (position); } } private void update () { if (this._command == null) { this._command = new Ft.Command (this.text); this._command.use_subshell = this._use_subshell; } else { this._command.line = this.text; this._command.use_subshell = this._use_subshell; } try { if (this._command.line != "") { this._command.validate (); } if (this.edit_icon != null) { this.edit_icon.icon_name = "document-edit-symbolic"; } this.tooltip_text = ""; this.remove_css_class ("error"); } catch (Ft.CommandError error) { if (this.edit_icon != null) { this.edit_icon.icon_name = "dialog-warning-symbolic"; } this.tooltip_text = error.message; this.add_css_class ("error"); } this.text_changed = false; } [GtkCallback] private void on_insert_variable_popover_selected (string variable_name, string variable_format_name) { var variable_display_name = to_camel_case (variable_name); var variable_format_display_name = to_camel_case (variable_format_name); this.insert_at_cursor (variable_format_display_name != "" ? "${%s:%s}".printf (variable_display_name, variable_format_display_name) : "${%s}".printf (variable_display_name)); } private void on_text_widget_state_flags_changed (Gtk.Widget widget, Gtk.StateFlags previous_state_flags) { var focused = Gtk.StateFlags.FOCUS_WITHIN in widget.get_state_flags (); if (!focused && this.text_changed) { this.update (); } else if (focused) { this.text_changed = true; this.remove_css_class ("error"); this.tooltip_text = ""; } } private void on_text_widget_changed (Gtk.Editable editable) { this.text_changed = true; } public override void dispose () { if (this.text_widget != null) { this.text_widget.state_flags_changed.disconnect (this.on_text_widget_state_flags_changed); this.text_widget.changed.disconnect (this.on_text_widget_changed); this.text_widget = null; } this._command = null; this.edit_icon = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/condition-group-widget.ui000066400000000000000000000044671520625676500327740ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/condition-group-widget.vala000066400000000000000000000544431520625676500333010ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public interface ExpressionWidget : Gtk.Widget { public abstract Ft.Expression? expression { get; set; } public abstract bool removable { get; set; } public signal void request_remove (); } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/condition-group-widget.ui")] public sealed class ConditionGroupWidget : Gtk.Widget, Ft.ExpressionWidget { private const int BRACKET_WIDTH = 24; private const int MIN_BRACKET_HEIGHT = 20; private const int MIN_WIDTH = 300; private const int NAT_WIDTH = 300; private const int PADDING = 0; private const int INDENT = 60; private const int SPACING = 6; private const float LINE_WIDTH = 2.0f; [CCode (notify = false)] public Ft.Expression? expression { get { return (Ft.Expression?) this._operation; } set { var operation = value as Ft.Operation; if (this._operation == operation) { return; } if (operation != null) { this._operation = operation; this._operator = operation.operator; } else { this._operation = null; } this.notify_property ("expression"); this.notify_property ("operator"); this.populate (); } } public Ft.Operator operator { get { return this._operator; } set { if (this._operator == value) { return; } this._operator = value; if (this._operation != null) { this._operation.operator = value; } this.update_operator_label (); } } public bool removable { get { return this._removable; } set { if (this._removable == value) { return; } this._removable = value; this.update_remove_buttons (); } } public bool is_nested { get; set; default = false; } [GtkChild] private unowned Gtk.Button operator_button; [GtkChild] private unowned Gtk.Box arguments_box; [GtkChild] private unowned Gtk.Box buttons_box; private Ft.Operation? _operation = null; private Ft.Operator _operator = Ft.Operator.AND; private bool _removable = false; private Ft.Gizmo? top_bracket; private Ft.Gizmo? bottom_bracket; static construct { set_css_name ("conditiongroup"); } construct { var top_bracket = new Ft.Gizmo ( ConditionGroupWidget.measure_bracket_child_cb, null, ConditionGroupWidget.snapshot_top_bracket_cb, null, null, null); top_bracket.focusable = false; top_bracket.set_parent (this); var bottom_bracket = new Ft.Gizmo ( ConditionGroupWidget.measure_bracket_child_cb, null, ConditionGroupWidget.snapshot_bottom_bracket_cb, null, null, null); bottom_bracket.focusable = false; bottom_bracket.set_parent (this); this.top_bracket = top_bracket; this.bottom_bracket = bottom_bracket; this.populate (); } private static Ft.ConditionGroupWidget? from_gizmo (Ft.Gizmo gizmo) { Gtk.Widget? widget = gizmo; while (widget != null) { var group = widget as Ft.ConditionGroupWidget; if (group != null) { return group; } widget = widget.get_parent (); } return null; } private static void measure_bracket_child_cb (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { var self = ConditionGroupWidget.from_gizmo (gizmo); if (self != null) { self.measure_bracket_child (gizmo, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = 0; minimum_baseline = -1; natural_baseline = -1; } } private static void snapshot_top_bracket_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = ConditionGroupWidget.from_gizmo (gizmo); if (self != null) { self.snapshot_top_bracket (gizmo, snapshot); } } private static void snapshot_bottom_bracket_cb (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { var self = ConditionGroupWidget.from_gizmo (gizmo); if (self != null) { self.snapshot_bottom_bracket (gizmo, snapshot); } } private Ft.ExpressionWidget create_condition (Ft.Expression? expression = null) { var child = new Ft.ConditionWidget (); child.removable = true; child.expression = expression; child.notify["expression"].connect (this.on_argument_notify_expression); child.request_remove.connect (this.on_argument_request_remove); return (Ft.ExpressionWidget) child; } private Ft.ExpressionWidget create_condition_group (Ft.Expression? expression = null) { var child = new Ft.ConditionGroupWidget (); child.operator = this.operator != Ft.Operator.AND ? Ft.Operator.AND : Ft.Operator.OR; child.removable = true; child.is_nested = true; child.request_remove.connect (this.on_argument_request_remove); return (Ft.ExpressionWidget) child; } private void populate () { var existing_arguments = new GLib.HashTable ( GLib.str_hash, GLib.str_equal); this.foreach_argument ( (argument) => { var argument_key = argument.expression != null ? ensure_string (argument.expression.to_string ()) : ""; existing_arguments.insert (argument_key, argument); this.arguments_box.remove (argument); }); var arguments_count = this._operation != null ? this._operation.arguments.length : 0; if (arguments_count == 0) { var argument = existing_arguments.lookup (""); if (argument == null) { argument = this.create_condition (); } this.arguments_box.prepend (argument); } else { for (var index = 0; index < arguments_count; index++) { var argument_expression = this._operation.arguments[index]; var argument_key = ensure_string (argument_expression?.to_string ()); var argument = existing_arguments.lookup (argument_key); var operation = argument_expression as Ft.Operation; var is_condition_group = operation != null && operation.operator.get_category () == Ft.OperatorCategory.LOGICAL; if (argument == null) { argument = is_condition_group ? this.create_condition_group (argument_expression) : this.create_condition (argument_expression); } else { existing_arguments.remove (argument_key); } this.arguments_box.insert_child_after (argument, this.buttons_box.get_prev_sibling ()); } } this.update_remove_buttons (); this.update_operator_label (); } private void foreach_argument (GLib.Func func) { var child = this.arguments_box.get_first_child (); while (child != null) { var argument = child as Ft.ExpressionWidget; var next_sibling = child.get_next_sibling (); if (argument != null) { func (argument); } child = next_sibling; } } private bool is_empty () { var arguments_count = 0; this.foreach_argument ( (argument) => { arguments_count++; }); return arguments_count == 0; } private void update_remove_buttons () { var arguments_count = 0; unowned Ft.ExpressionWidget? first_argument = null; this.foreach_argument ( (argument) => { arguments_count++; if (arguments_count == 1) { first_argument = argument; } else { argument.removable = true; } }); if (first_argument != null) { first_argument.removable = this.removable || this.is_nested || arguments_count > 1; } } private void update_operator_label () { this.operator_button.label = this.operator == Ft.Operator.AND ? _("AND") : _("OR"); } private void update_expression () { Ft.Expression[] arguments = {}; this.foreach_argument ( (argument) => { var argument_expression = argument.expression; // TODO: validate that arguments are boolean if (argument_expression != null) { arguments += argument_expression; } }); this._operation = arguments.length > 0 ? new Ft.Operation.with_argv (this.operator, arguments) : null; this.notify_property ("expression"); } private void measure_bracket_child (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = MIN_BRACKET_HEIGHT; natural = MIN_BRACKET_HEIGHT; minimum_baseline = 0; natural_baseline = 0; } private void snapshot_bracket (Ft.Gizmo gizmo, Gtk.Snapshot snapshot, bool is_top) { var width = (float) gizmo.get_width (); var height = (float) gizmo.get_height (); var border_radius = 8.0f; var padding = LINE_WIDTH / 2.0f; var x = width / 2.0f; var y = border_radius + padding; var color = gizmo.get_color (); color.alpha *= 0.2f; var path_builder = new Gsk.PathBuilder (); path_builder.move_to (x, height - padding); path_builder.line_to (x, y + border_radius); path_builder.quad_to (x, y, x + border_radius, y); path_builder.line_to (width - padding, y); var stroke = new Gsk.Stroke (LINE_WIDTH); stroke.set_line_cap (Gsk.LineCap.ROUND); if (!is_top) { snapshot.translate ({ 0.0f, height }); snapshot.scale (1.0f, -1.0f); } snapshot.append_stroke (path_builder.to_path (), stroke, color); } private void snapshot_top_bracket (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { this.snapshot_bracket (gizmo, snapshot, true); } private void snapshot_bottom_bracket (Ft.Gizmo gizmo, Gtk.Snapshot snapshot) { this.snapshot_bracket (gizmo, snapshot, false); } private void on_argument_notify_expression (GLib.Object object, GLib.ParamSpec pspec) { this.update_expression (); } private void on_argument_request_remove (Ft.ExpressionWidget argument) { this.arguments_box.remove ((Gtk.Widget) argument); if (!this.is_empty ()) { this.update_remove_buttons (); this.update_expression (); } else { if (this.is_nested) { this.request_remove (); } else { if (this._operation != null) { this.expression = null; } else { // The last child just got removed. Need to repopulate. this.populate (); } if (this.removable) { this.request_remove (); } } } } [GtkCallback] private void on_operator_button_clicked (Gtk.Button button) { this.operator = this.operator == Ft.Operator.AND ? Ft.Operator.OR : Ft.Operator.AND; } [GtkCallback] private void on_add_condition_button_clicked (Gtk.Button button) { var argument = this.create_condition (); var sibling = this.buttons_box.get_prev_sibling (); this.arguments_box.insert_child_after (argument, sibling); this.update_remove_buttons (); } [GtkCallback] private void on_add_condition_group_button_clicked (Gtk.Button button) { var argument = this.create_condition_group (); var sibling = this.buttons_box.get_prev_sibling (); this.arguments_box.insert_child_after (argument, sibling); this.update_remove_buttons (); } private void calculate_height_for_width (int avaliable_width, out int minimum_height, out int natural_height) { var tmp_minimum_height = 0; var tmp_natural_height = 0; this.operator_button.measure (Gtk.Orientation.VERTICAL, INDENT, out tmp_minimum_height, out tmp_natural_height, null, null); minimum_height = tmp_minimum_height + 2 * MIN_BRACKET_HEIGHT; natural_height = tmp_natural_height + 2 * MIN_BRACKET_HEIGHT; if (avaliable_width >= 0) { this.arguments_box.measure (Gtk.Orientation.VERTICAL, avaliable_width - INDENT - SPACING, out tmp_minimum_height, out tmp_natural_height, null, null); minimum_height = int.max (minimum_height, tmp_minimum_height); natural_height = int.max (natural_height, tmp_natural_height); } else { this.arguments_box.measure (Gtk.Orientation.VERTICAL, MIN_WIDTH - INDENT - SPACING, out tmp_minimum_height, null, null, null); this.arguments_box.measure (Gtk.Orientation.VERTICAL, -1, null, out tmp_natural_height, null, null); minimum_height = int.max (minimum_height, tmp_minimum_height); natural_height = int.max (natural_height, tmp_natural_height); } } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { if (orientation == Gtk.Orientation.HORIZONTAL) { minimum = MIN_WIDTH; natural = for_size != -1 ? int.max (for_size, MIN_WIDTH) : MIN_WIDTH; } else { this.calculate_height_for_width (for_size, out minimum, out natural); } if (natural < minimum) { natural = minimum; } minimum_baseline = -1; natural_baseline = -1; } public override void size_allocate (int width, int height, int baseline) { // var is_ltr = this.get_direction () != Gtk.TextDirection.RTL; // TODO var operator_allocation = Gtk.Allocation () { width = INDENT }; this.operator_button.measure (Gtk.Orientation.VERTICAL, operator_allocation.width, null, out operator_allocation.height, null, null); var top_bracket_allocation = Gtk.Allocation () { x = (INDENT - BRACKET_WIDTH) / 2, y = 0, width = BRACKET_WIDTH, height = (height - operator_allocation.height - SPACING) / 2 }; var bottom_bracket_allocation = Gtk.Allocation () { x = top_bracket_allocation.x, y = height - top_bracket_allocation.height, width = top_bracket_allocation.width, height = top_bracket_allocation.height }; var arguments_allocation = Gtk.Allocation () { width = width - INDENT - SPACING, height = height }; this.arguments_box.measure (Gtk.Orientation.VERTICAL, arguments_allocation.width, null, out arguments_allocation.height, null, null); operator_allocation.x = INDENT - operator_allocation.width; arguments_allocation.x = operator_allocation.x + SPACING + operator_allocation.width; operator_allocation.y = (height - operator_allocation.height) / 2; arguments_allocation.y = (height - arguments_allocation.height) / 2; this.top_bracket.allocate_size (top_bracket_allocation, -1); this.operator_button.allocate_size (operator_allocation, -1); this.bottom_bracket.allocate_size (bottom_bracket_allocation, -1); this.arguments_box.allocate_size (arguments_allocation, -1); } public override void dispose () { this._operation = null; if (this.top_bracket != null) { this.top_bracket.unparent (); this.top_bracket = null; } if (this.bottom_bracket != null) { this.bottom_bracket.unparent (); this.bottom_bracket = null; } // HACK: Without this children do not get disposed properly this.dispose_template (typeof (Ft.ConditionGroupWidget)); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/condition-widget.ui000066400000000000000000000117621520625676500316360ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/condition-widget.vala000066400000000000000000000456261520625676500321520ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/condition-widget.ui")] private class ConditionWidget : Gtk.Box, Ft.ExpressionWidget { // NOTE: Keep choices synced with the .ui file. private const Ft.Operator[] ENUM_OPERATOR_CHOICES = { Ft.Operator.EQ, Ft.Operator.NOT_EQ }; private const Ft.Operator[] NUMERICAL_OPERATOR_CHOICES = { Ft.Operator.EQ, Ft.Operator.GT, Ft.Operator.LT }; private const Ft.State[] STATE_CHOICES = { Ft.State.POMODORO, Ft.State.BREAK, Ft.State.STOPPED }; private const bool[] BOOLEAN_CHOICES = { true, false }; private const int64[] INTERVAL_UNIT_CHOICES = { Ft.Interval.MINUTE, Ft.Interval.SECOND, Ft.Interval.HOUR }; private class FieldItem : GLib.Object { public string name { get; construct; } public string label { get; construct; } public Ft.VariableSpec? variable_spec { get; construct; } public FieldItem (string name, string label) { GLib.Object ( name: name, label: label, variable_spec: Ft.find_variable (name) ); } } /** * This widget can only represent `Comparison` expressions at the moment. It's possible * to extend it to handle more types of expressions, like using variables directly. */ [CCode (notify = false)] public Ft.Expression? expression { get { return (Ft.Expression?) this._comparison; } set { var comparison = value as Ft.Comparison; if (value != null && comparison == null) { comparison = new Ft.Comparison.is_true (value); } if (this._comparison == comparison) { return; } this._comparison = comparison; this.populate (); this.notify_property ("expression"); } } public bool removable { get; set; default = false; } [GtkChild] private unowned Gtk.FlowBox fields; [GtkChild] private unowned Gtk.DropDown field_dropdown; [GtkChild] private unowned Gtk.DropDown enum_operator_dropdown; [GtkChild] private unowned Gtk.DropDown numerical_operator_dropdown; [GtkChild] private unowned Gtk.DropDown state_dropdown; [GtkChild] private unowned Gtk.DropDown boolean_dropdown; [GtkChild] private unowned Gtk.SpinButton interval_spinbutton; [GtkChild] private unowned Gtk.DropDown interval_unit_dropdown; private Ft.Comparison? _comparison = null; private uint update_idle_id = 0; static construct { set_css_name ("condition"); } construct { var fields_list = new GLib.ListStore (typeof (FieldItem)); fields_list.splice (0, 0, { /* translators: No field selected when defining a condition. */ new FieldItem ("", _("Select Field…")), new FieldItem ("state", _("State")), new FieldItem ("is-started", _("Started")), new FieldItem ("is-paused", _("Paused")), new FieldItem ("is-running", _("Running")), new FieldItem ("is-finished", _("Finished")), new FieldItem ("duration", _("Duration")), // TODO: status, elapsed, remaining, offset? }); this.field_dropdown.expression = new Gtk.PropertyExpression ( typeof (FieldItem), null, "label"); this.field_dropdown.model = fields_list; this.update_operator_fields (); this.update_value_fields (); /** * XXX: Having each field in it's own .ui file + constructing them dynamically * would be a cleaner approach. */ Gtk.Widget[] fields = { this.field_dropdown, this.enum_operator_dropdown, this.numerical_operator_dropdown, this.state_dropdown, this.boolean_dropdown, this.interval_spinbutton, this.interval_unit_dropdown, }; foreach (var field in fields) { field.@ref (); } this.fields.remove_all (); this.fields.append (this.field_dropdown); } private static int64 guess_interval_unit (int64 interval) { if (interval % Ft.Interval.HOUR == 0) { return Ft.Interval.HOUR; } if (interval % Ft.Interval.MINUTE == 0) { return Ft.Interval.MINUTE; } return Ft.Interval.SECOND; } private FieldItem? get_selected_field_item () { var field_item = this.field_dropdown.selected_item as FieldItem; return field_item != null && field_item.name != "" ? field_item : null; } private Ft.Operator get_selected_operator () { if (this.enum_operator_dropdown.parent != null) { var index = this.enum_operator_dropdown.selected; assert (index < ENUM_OPERATOR_CHOICES.length); return ENUM_OPERATOR_CHOICES[index]; } if (this.numerical_operator_dropdown.parent != null) { var index = this.numerical_operator_dropdown.selected; assert (index < NUMERICAL_OPERATOR_CHOICES.length); return NUMERICAL_OPERATOR_CHOICES[index]; } return Ft.Operator.EQ; } private Ft.Value? get_selected_value () { if (this.state_dropdown.parent != null) { var index = this.state_dropdown.selected; assert (index < STATE_CHOICES.length); return new Ft.StateValue (STATE_CHOICES[index]); } if (this.boolean_dropdown.parent != null) { var index = this.boolean_dropdown.selected; assert (index < BOOLEAN_CHOICES.length); return new Ft.BooleanValue (BOOLEAN_CHOICES[index]); } if (this.interval_spinbutton.parent != null) { var index = this.interval_unit_dropdown.selected; assert (index < INTERVAL_UNIT_CHOICES.length); var interval = Ft.Interval.from_value ((int) this.interval_spinbutton.value, INTERVAL_UNIT_CHOICES[index]); return new Ft.IntervalValue (interval); } return null; } private void update_expression () { var selected_field_item = this.get_selected_field_item (); var selected_operator = this.get_selected_operator (); var selected_value = this.get_selected_value (); if (this.update_idle_id != 0) { this.remove_tick_callback (this.update_idle_id); this.update_idle_id = 0; } if (selected_field_item != null && selected_field_item?.name != null && selected_value != null) { this._comparison = new Ft.Comparison ( new Ft.Variable (selected_field_item.name), selected_operator, new Ft.Constant (selected_value) ); } else { this._comparison = null; } this.notify_property ("expression"); } private void queue_update_expression () { if (this.update_idle_id != 0) { return; } this.update_idle_id = this.add_tick_callback (() => { this.update_idle_id = 0; this.update_expression (); return GLib.Source.REMOVE; }); } private bool select_field (string name) { var fields_list = this.field_dropdown.model; var n_items = fields_list.get_n_items (); for (var position = 0; position < n_items; position++) { var item = (FieldItem) fields_list.get_item (position); if (item.name == name) { this.field_dropdown.selected = position; return true; } } return false; } private bool select_operator (Ft.Operator operator) { if (this.enum_operator_dropdown.parent != null) { for (var index = 0; index < ENUM_OPERATOR_CHOICES.length; index++) { if (ENUM_OPERATOR_CHOICES[index] == operator) { this.enum_operator_dropdown.selected = index; return true; } } } if (this.numerical_operator_dropdown.parent != null) { for (var index = 0; index < NUMERICAL_OPERATOR_CHOICES.length; index++) { if (NUMERICAL_OPERATOR_CHOICES[index] == operator) { this.numerical_operator_dropdown.selected = index; return true; } } } return false; } private bool select_value (Ft.Value? value) { if (value is Ft.StateValue) { var state_value = ((Ft.StateValue) value).data; for (var index = 0; index < STATE_CHOICES.length; index++) { if (STATE_CHOICES[index] == state_value) { this.state_dropdown.selected = index; return true; } } assert_not_reached (); } if (value is Ft.BooleanValue) { var boolean_value = ((Ft.BooleanValue) value).data; for (var index = 0; index < BOOLEAN_CHOICES.length; index++) { if (BOOLEAN_CHOICES[index] == boolean_value) { this.boolean_dropdown.selected = index; return true; } } assert_not_reached (); } if (value is Ft.IntervalValue) { var interval_value = ((Ft.IntervalValue) value).data; var interval_unit = guess_interval_unit (interval_value); interval_value /= interval_unit; for (var index = 0; index < INTERVAL_UNIT_CHOICES.length; index++) { if (INTERVAL_UNIT_CHOICES[index] == interval_unit) { this.interval_spinbutton.value = (double) interval_value; this.interval_unit_dropdown.selected = index; return true; } } assert_not_reached (); } return false; } private void populate () { if (this._comparison != null) { var argument_lhs = this._comparison.argument_lhs as Ft.Variable; var argument_rhs = this._comparison.argument_rhs as Ft.Constant; this.select_field (argument_lhs?.name); this.select_operator (this._comparison.operator); this.select_value (argument_rhs?.value); this.update_operator_fields (); this.update_value_fields (); } } private void update_empty_item () { var selected_field_item = this.get_selected_field_item (); if (selected_field_item == null || selected_field_item.name == "") { return; } // TODO: Wrap model with Gtk.FilterListModel and toggle // first item on and off. var model = (GLib.ListStore) this.field_dropdown.model; var first_item = (FieldItem) model.get_item (0); if (first_item.name == "") { model.remove (0); } } private void update_operator_fields () { var selected_field_item = this.get_selected_field_item (); var selected_operator = this.get_selected_operator (); var value_type = selected_field_item != null ? (GLib.Type?) selected_field_item.variable_spec.value_type : (GLib.Type?) null; if (this.enum_operator_dropdown.parent != null) { this.fields.remove (this.enum_operator_dropdown); } if (this.numerical_operator_dropdown.parent != null) { this.fields.remove (this.numerical_operator_dropdown); } if (value_type == typeof (Ft.StateValue)) { this.fields.insert (this.enum_operator_dropdown, 1); } if (value_type == typeof (Ft.IntervalValue)) { this.fields.insert (this.numerical_operator_dropdown, 1); } if (!this.select_operator (selected_operator)) { this.enum_operator_dropdown.selected = 0; this.numerical_operator_dropdown.selected = 0; } this.queue_update_expression (); } private void update_value_fields () { var selected_field_item = this.get_selected_field_item (); var selected_value = this.get_selected_value (); var value_type = selected_field_item != null ? (GLib.Type?) selected_field_item.variable_spec.value_type : (GLib.Type?) null; if (this.state_dropdown.parent != null) { this.fields.remove (this.state_dropdown); } if (this.boolean_dropdown.parent != null) { this.fields.remove (this.boolean_dropdown); } if (this.interval_spinbutton.parent != null) { this.fields.remove (this.interval_spinbutton); } if (this.interval_unit_dropdown.parent != null) { this.fields.remove (this.interval_unit_dropdown); } if (value_type == typeof (Ft.StateValue)) { this.fields.append (this.state_dropdown); } if (value_type == typeof (Ft.BooleanValue)) { this.fields.append (this.boolean_dropdown); } if (value_type == typeof (Ft.IntervalValue)) { this.fields.append (this.interval_spinbutton); this.fields.append (this.interval_unit_dropdown); } if (!this.select_value (selected_value)) { this.state_dropdown.selected = 0; this.boolean_dropdown.selected = 0; } this.queue_update_expression (); } [GtkCallback] private void on_field_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.field_dropdown.model == null) { return; // Not initialized yet. } this.update_empty_item (); this.update_operator_fields (); this.update_value_fields (); } [GtkCallback] private void on_enum_operator_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.enum_operator_dropdown.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_numerical_operator_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.numerical_operator_dropdown.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_state_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.state_dropdown.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_boolean_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.boolean_dropdown.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_interval_adjustment_value_changed () { if (this.interval_spinbutton.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_interval_unit_notify_selected_item (GLib.Object object, GLib.ParamSpec pspec) { if (this.interval_unit_dropdown.parent == null) { return; } this.queue_update_expression (); } [GtkCallback] private void on_remove_button_clicked (Gtk.Button button) { this.request_remove (); } public override void dispose () { Gtk.Widget fields[] = { this.field_dropdown, this.enum_operator_dropdown, this.numerical_operator_dropdown, this.state_dropdown, this.boolean_dropdown, this.interval_spinbutton, this.interval_unit_dropdown, }; foreach (var field in fields) { field.@unref (); } if (this.update_idle_id != 0) { this.remove_tick_callback (this.update_idle_id); this.update_idle_id = 0; } this.fields.remove_all (); this._comparison = null; // HACK: Without this children do not get disposed properly this.dispose_template (typeof (Ft.ConditionGroupWidget)); base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/variable-popover.ui000066400000000000000000000174751520625676500316530ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/action/variable-popover.vala000066400000000000000000000242131520625676500321450ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { private class VariableItem : GLib.Object { public string display_name { get { return this._display_name; } } public string name { get { return this.spec.name; } } public string description { get { return this.spec.description; } } public GLib.Type value_type { get { return this.spec.value_type; } } private Ft.VariableSpec spec; private string _display_name; public VariableItem (Ft.VariableSpec spec) { this.spec = spec; this._display_name = to_camel_case (spec.name); } } private class FormatItem : GLib.Object { public string display_name { get { return this._display_name; } } public string name { get { return this._name; } } private string _display_name; private string _name; public FormatItem (string variable_format) { this._name = variable_format; this._display_name = to_camel_case (variable_format); } } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/action/variable-popover.ui")] public class VariablePopover : Gtk.Popover { [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.ListView variables_listview; [GtkChild] private unowned Gtk.ListView formats_listview; [GtkChild] private unowned Gtk.Label variable_name_label; [GtkChild] private unowned Gtk.Label variable_description_label; private Gtk.SingleSelection? variables_model = null; private Gtk.SingleSelection? formats_model = null; private static GLib.HashTable last_used_formats = null; construct { this.variables_model = new Gtk.SingleSelection (this.create_variables_model ()); this.variables_model.autoselect = false; this.variables_model.can_unselect = true; this.variables_model.selection_changed.connect (this.on_variables_model_selection_changed); this.variables_listview.model = this.variables_model; this.reset (); } private string get_preferred_format (GLib.Type value_type) { string? format = null; // TODO prioritize selected format used in the command line over last used ones. if (last_used_formats == null) { last_used_formats = new GLib.HashTable (GLib.direct_hash, GLib.direct_equal); } else { format = last_used_formats.lookup (value_type); } if (format == null) { format = Ft.get_default_value_format (value_type); } return ensure_string (format); } private uint index_format (string format_name) { var model = this.formats_model.model; var n_items = model.get_n_items (); for (var position = 0U; position < n_items; position++) { var item = (FormatItem) model.get_item (position); if (item.name == format_name) { return position; } } return 0U; } private GLib.ListModel create_variables_model () { var model = new GLib.ListStore (typeof (VariableItem)); foreach (unowned var variable in Ft.list_variables ()) { model.append (new VariableItem (variable)); } return model; } private GLib.ListModel create_formats_model (GLib.Type value_type) { var model = new GLib.ListStore (typeof (FormatItem)); foreach (unowned var variable_format in Ft.list_value_formats (value_type)) { model.append (new FormatItem (variable_format)); } return model; } public void reset () { this.variables_model.unselect_item (this.variables_model.selected); } [GtkCallback] private string get_variable_display_name (GLib.Object? item) { var variable_item = (VariableItem?) item; return variable_item?.display_name; } [GtkCallback] private void setup_format_item (GLib.Object object) { var check_button = new Gtk.CheckButton (); check_button.add_css_class ("monospace"); var list_item = (Gtk.ListItem) object; list_item.child = check_button; } [GtkCallback] private void bind_format_item (GLib.Object object) { var list_item = (Gtk.ListItem) object; var check_button = (Gtk.CheckButton) list_item.child; check_button.active = list_item.selected; var format_item = (FormatItem) list_item.item; format_item.bind_property ("display-name", check_button, "label", GLib.BindingFlags.SYNC_CREATE); var toggled_id = check_button.toggled.connect ( () => { if (check_button.active) { this.formats_model.select_item (list_item.position, true); } else { check_button.active = true; } }); var notify_selected_id = list_item.notify["selected"].connect ( (obj, pspec) => { if (check_button.active != list_item.selected) { GLib.SignalHandler.block (check_button, toggled_id); check_button.active = list_item.selected; GLib.SignalHandler.unblock (check_button, toggled_id); } }); list_item.set_data ("toggled-id", toggled_id); list_item.set_data ("notify-selected-id", notify_selected_id); } [GtkCallback] private void unbind_format_item (GLib.Object object) { var list_item = (Gtk.ListItem) object; var toggled_id = list_item.get_data ("toggled-id"); var notify_selected_id = list_item.get_data ("notify-selected-id"); list_item.child.disconnect (toggled_id); list_item.disconnect (notify_selected_id); } [GtkCallback] private void on_back_button_clicked () { this.reset (); } [GtkCallback] private void on_insert_variable_button_clicked () { var variable_item = (VariableItem?) this.variables_model.selected_item; var format_item = (FormatItem?) this.formats_model.selected_item; if (variable_item != null) { var variable_name = variable_item.name; var format_name = format_item != null ? format_item.name : ""; if (format_name == Ft.get_default_value_format (variable_item.value_type)) { format_name = ""; } this.selected (variable_name, format_name); } } private void on_formats_model_selection_changed () requires (last_used_formats != null) { var variable_item = (VariableItem?) this.variables_model.selected_item; var format_item = (FormatItem?) this.formats_model.selected_item; if (variable_item != null && format_item != null) { last_used_formats.insert (variable_item.value_type, format_item.name); } } private void on_variables_model_selection_changed (uint position, uint n_items) { var variable_item = (VariableItem?) this.variables_model.selected_item; if (variable_item != null) { var formats_model = this.create_formats_model (variable_item.value_type); var preferred_format = this.get_preferred_format (variable_item.value_type); this.formats_model = new Gtk.SingleSelection (formats_model); this.formats_model.autoselect = true; this.formats_model.can_unselect = false; this.formats_model.select_item (this.index_format (preferred_format), true); this.formats_model.selection_changed.connect (this.on_formats_model_selection_changed); this.formats_listview.model = this.formats_model; this.formats_listview.visible = this.formats_model.n_items > 1; this.variable_name_label.label = variable_item.name; this.variable_description_label.label = variable_item.description; this.stack.visible_child_name = "details"; } else { this.stack.visible_child_name = "list"; } } public signal void selected (string variable_name, string variable_format_name) { this.visible = false; } public override void closed () { this.reset (); } public override void dispose () { if (this.variables_model != null) { this.variables_model.selection_changed.disconnect (this.on_variables_model_selection_changed); this.variables_model = null; } if (this.formats_model != null) { this.formats_model.selection_changed.disconnect (this.on_formats_model_selection_changed); this.formats_model = null; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/preferences-panel-automation.ui000066400000000000000000000036451520625676500326670ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/automation/preferences-panel-automation.vala000066400000000000000000000132251520625676500331700ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/automation/preferences-panel-automation.ui")] public class PreferencesPanelAutomation : Ft.PreferencesPanel { [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Adw.PreferencesRow add_row; private Ft.ActionManager? action_manager = null; private GLib.HashTable? rows = null; private unowned Gtk.ListBox listbox = null; private uint update_idle_id = 0; construct { this.rows = new GLib.HashTable ( GLib.str_hash, GLib.str_equal); this.action_manager = new Ft.ActionManager (); this.action_manager.model.items_changed.connect (this.on_items_changed); this.listbox = (Gtk.ListBox) this.add_row.parent; this.listbox.selection_mode = Gtk.SelectionMode.NONE; this.listbox.set_sort_func (sort_func); this.listbox.row_activated.connect (this.on_listbox_row_activated); this.update (); } private static int sort_func (Gtk.ListBoxRow row_1, Gtk.ListBoxRow row_2) { var action_row_1 = row_1 as Ft.ActionListBoxRow; var action_row_2 = row_2 as Ft.ActionListBoxRow; if (action_row_1 == null) { return 1; } if (action_row_2 == null) { return -1; } return (int) action_row_1.sort_order - (int) action_row_2.sort_order; } private void update () { var model = this.action_manager.model; var n_items = model.n_items; var to_remove = new GLib.GenericSet (GLib.direct_hash, GLib.direct_equal); if (this.update_idle_id != 0) { this.remove_tick_callback (this.update_idle_id); this.update_idle_id = 0; } this.rows.@foreach ( (uuid, row) => { to_remove.add (row); }); for (var position = 0U; position < n_items; position++) { var action = (Ft.Action?) model.get_item (position); assert (action != null); var row = this.rows.lookup (action.uuid); if (row != null) { row.action = action; } else { row = new Ft.ActionListBoxRow (action); row.move_row.connect (this.on_move_row); this.listbox.append (row); this.rows.insert (action.uuid, row); } row.sort_order = position; to_remove.remove (row); } to_remove.@foreach ( (row) => { row.move_row.disconnect (this.on_move_row); this.rows.remove (row.action.uuid); this.listbox.remove (row); }); this.listbox.invalidate_sort (); // TODO disconnect signals of removed actions? } private void open_action_edit_window (Ft.Action action) { var window = new Ft.ActionEditWindow (action.uuid); window.set_transient_for ((Gtk.Window?) this.get_root ()); window.present (); } private void on_items_changed (uint position, uint removed, uint added) { if (this.update_idle_id == 0) { this.update_idle_id = this.add_tick_callback (() => { this.update_idle_id = 0; this.update (); return GLib.Source.REMOVE; }); } } private void on_listbox_row_activated (Gtk.ListBoxRow row) { if (row == this.add_row) { this.open_action_edit_window (new Ft.EventAction ()); } else { this.open_action_edit_window (((Ft.ActionListBoxRow) row).action); } } private void on_move_row (Ft.ActionListBoxRow row, Ft.ActionListBoxRow destination_row) { this.action_manager.model.move_action (row.action.uuid, destination_row.sort_order); this.update (); } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { if (this.update_idle_id != 0) { this.remove_tick_callback (this.update_idle_id); this.update_idle_id = 0; } if (this.action_manager != null) { this.action_manager.model.items_changed.disconnect (this.on_items_changed); } if (this.listbox != null) { this.listbox.row_activated.disconnect (this.on_listbox_row_activated); } if (this.rows != null) { this.rows.remove_all (); this.rows = null; } this.rows = null; this.listbox = null; this.action_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/integrations/000077500000000000000000000000001520625676500250725ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/integrations/preferences-panel-integrations.ui000066400000000000000000000036421520625676500335400ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/integrations/preferences-panel-integrations.vala000066400000000000000000000025741520625676500340510ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/integrations/preferences-panel-integrations.ui")] public class PreferencesPanelIntegrations : Ft.PreferencesPanel { [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Adw.SwitchRow autostart_switchrow; [GtkChild] private unowned Gtk.Label autostart_label; private GLib.Settings? settings = null; construct { this.settings = Ft.get_settings (); this.settings.bind ( "autostart", this.autostart_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); var background_manager = new Ft.BackgroundManager (); background_manager.bind_property ( "autostart-allowed", this.autostart_label, "visible", GLib.BindingFlags.SYNC_CREATE); } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/000077500000000000000000000000001520625676500262205ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui000066400000000000000000000112701520625676500340110ustar00rootroot00000000000000 accelerator-chooser-window.vala000066400000000000000000000146721520625676500342510ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ /** * Based on cc-keyboard-shortcut-editor.c from gnome-control-center */ namespace Ft { // TODO: make it a dialog window [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/keyboard-shortcuts/accelerator-chooser-window.ui")] public class AcceleratorChooserWindow : Adw.Window { private enum Mode { CAPTURE, REVIEW } public string description { get { return this.description_label.label; } set { this.description_label.label = value; } } public string accelerator { get; set; } [GtkChild] private unowned Gtk.Button cancel_button; [GtkChild] private unowned Gtk.Button set_button; [GtkChild] private unowned Gtk.Label description_label; [GtkChild] private unowned Gtk.ShortcutLabel accelerator_label; [GtkChild] private unowned Gtk.Picture capture_image; [GtkChild] private unowned Gtk.Label capture_hint; private Mode mode; private bool system_shortcuts_inhibited = false; private Ft.KeyboardManager? keyboard_manager; construct { this.keyboard_manager = new Ft.KeyboardManager (); this.set_mode (Mode.CAPTURE); } public AcceleratorChooserWindow (string description, string accelerator) { this.description = description; this.accelerator = accelerator; } private void inhibit_system_shortcuts () { this.keyboard_manager?.inhibit (); if (this.system_shortcuts_inhibited) { return; } var toplevel = this.get_native ().get_surface () as Gdk.Toplevel; if (toplevel != null) { toplevel.inhibit_system_shortcuts (null); this.system_shortcuts_inhibited = true; } } private void uninhibit_system_shortcuts () { this.keyboard_manager?.uninhibit (); if (!this.system_shortcuts_inhibited) { return; } var toplevel = this.get_native ().get_surface () as Gdk.Toplevel; if (toplevel != null) { toplevel.restore_system_shortcuts (); this.system_shortcuts_inhibited = false; } } private void set_mode (Mode mode) { this.mode = mode; switch (mode) { case Mode.CAPTURE: this.cancel_button.visible = false; this.set_button.visible = false; this.accelerator_label.visible = false; this.capture_image.visible = true; this.capture_hint.visible = true; this.inhibit_system_shortcuts (); break; case Mode.REVIEW: this.cancel_button.visible = true; this.set_button.visible = true; this.accelerator_label.visible = true; this.capture_image.visible = false; this.capture_hint.visible = false; break; default: assert_not_reached (); } if (mode != Mode.CAPTURE) { this.uninhibit_system_shortcuts (); } } [GtkCallback] private void on_cancel_button_clicked () { this.response (Gtk.ResponseType.CANCEL); } [GtkCallback] private void on_set_button_clicked () { this.response (Gtk.ResponseType.APPLY); } [GtkCallback] private bool on_key_pressed (Gtk.EventControllerKey event_controller, uint keyval, uint keycode, Gdk.ModifierType state) { var event = event_controller.get_current_event () as Gdk.KeyEvent; if (this.mode != Mode.CAPTURE) { if (keyval == Gdk.Key.Return) { this.response (Gtk.ResponseType.APPLY); return Gdk.EVENT_STOP; } return Gdk.EVENT_PROPAGATE; } if (event == null || event.is_modifier ()) { return Gdk.EVENT_STOP; } var accelerator = Ft.Accelerator.from_keycode (keycode, state, event_controller.get_group ()); if (accelerator.modifiers == Gdk.ModifierType.NO_MODIFIER_MASK) { switch (keyval) { /* A single Escape press aborts editing */ case Gdk.Key.Escape: this.response (Gtk.ResponseType.CANCEL); return Gdk.EVENT_STOP; /* Backspace disables the current shortcut */ case Gdk.Key.BackSpace: this.accelerator = ""; this.response (Gtk.ResponseType.APPLY); return Gdk.EVENT_STOP; default: break; } } if (accelerator.is_valid ()) { this.accelerator = accelerator.to_string (); this.set_mode (Mode.REVIEW); } return Gdk.EVENT_STOP; } public override void map () { base.map (); if (this.mode == Mode.CAPTURE) { this.inhibit_system_shortcuts (); } } public override void unmap () { this.uninhibit_system_shortcuts (); base.unmap (); } public virtual signal void response (int response_id) { this.close (); } public override void dispose () { this.keyboard_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/accelerator-row.ui000066400000000000000000000015021520625676500316460ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/accelerator-row.vala000066400000000000000000000047201520625676500321610ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/keyboard-shortcuts/accelerator-row.ui")] public class AcceleratorRow : Adw.ActionRow { [CCode (notify = false)] public string accelerator { get { return this._accelerator; } construct set { if (value == null) { value = ""; } if (this._accelerator != value) { this._accelerator = value; this.notify_property ("accelerator"); } this.update_label (); } } public string description { get; set; } [GtkChild] private unowned Gtk.Label accelerator_label; private string _accelerator; construct { this.accelerator_label.set_direction (Gtk.TextDirection.LTR); } private void update_label () { var accelerator = this._accelerator != "" ? Ft.Accelerator.from_string (this._accelerator) : Ft.Accelerator.empty (); if (accelerator.is_empty ()) { this.accelerator_label.label = _("Disabled"); this.accelerator_label.add_css_class ("dim-label"); } else { this.accelerator_label.label = accelerator.get_label (); this.accelerator_label.remove_css_class ("dim-label"); } } private Ft.AcceleratorChooserWindow create_accelerator_chooser () { var chooser = new Ft.AcceleratorChooserWindow (this.description, this.accelerator); chooser.transient_for = (Gtk.Window) this.get_root (); return chooser; } private void on_chooser_response (Ft.AcceleratorChooserWindow chooser, int response_id) { if (response_id != Gtk.ResponseType.APPLY) { return; } this.accelerator = chooser.accelerator; } [GtkCallback] public void on_activated () { var chooser = this.create_accelerator_chooser (); chooser.response.connect (this.on_chooser_response); chooser.present (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/accelerator.vala000066400000000000000000000270721520625676500313610ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { public struct Accelerator { private const uint[] FORBIDDEN_KEYVALS = { Gdk.Key.Home, Gdk.Key.Left, Gdk.Key.Up, Gdk.Key.Right, Gdk.Key.Down, Gdk.Key.Page_Up, Gdk.Key.Page_Down, Gdk.Key.End, Gdk.Key.Tab, Gdk.Key.Escape, Gdk.Key.KP_Enter, Gdk.Key.Return, Gdk.Key.space, Gdk.Key.BackSpace, Gdk.Key.Mode_switch }; public uint keycode; public uint keyval; public Gdk.ModifierType modifiers; public static Ft.Accelerator empty () { return Ft.Accelerator () { keycode = 0, keyval = 0, modifiers = Gdk.ModifierType.NO_MODIFIER_MASK }; } public static Ft.Accelerator from_keycode (uint keycode, Gdk.ModifierType modifiers, uint group) { var accelerator = Ft.Accelerator () { keycode = keycode, keyval = 0, modifiers = modifiers }; return accelerator.normalize (group); } public static Ft.Accelerator from_string (string accelerator_string) { int index = 0; int modifier_position = -1; int keyval_position = 0; unichar chr; var modifiers = Gdk.ModifierType.NO_MODIFIER_MASK; while (accelerator_string.get_next_char (ref index, out chr)) { if (chr == '<' && modifier_position == -1) { modifier_position = index; } else if (chr == '>' && modifier_position >= 0) { var modifier_name = accelerator_string.slice (modifier_position, index - 1); switch (modifier_name.down ()) { case "ctrl": case "control": modifiers |= Gdk.ModifierType.CONTROL_MASK; break; case "⌘": case "meta": modifiers |= Gdk.ModifierType.META_MASK; break; case "alt": modifiers |= Gdk.ModifierType.ALT_MASK; break; case "shift": modifiers |= Gdk.ModifierType.SHIFT_MASK; break; case "super": modifiers |= Gdk.ModifierType.SUPER_MASK; break; case "hyper": modifiers |= Gdk.ModifierType.HYPER_MASK; break; default: GLib.warning ("Unknown modifier name: '%s'", modifier_name); break; } modifier_position = -1; keyval_position = index; } } return Ft.Accelerator () { keycode = 0, keyval = Gdk.keyval_from_name (accelerator_string.slice (keyval_position, index)), modifiers = modifiers }; } public bool is_empty () { return this.keycode == 0 && this.keyval == 0; } public bool is_valid () { var keyval = this.keyval; var modifiers = this.modifiers; if (this.is_empty ()) { return true; } if (modifiers == Gdk.ModifierType.NO_MODIFIER_MASK || modifiers == Gdk.ModifierType.SHIFT_MASK) { /* Check for typing collision */ if ((keyval >= Gdk.Key.a && keyval <= Gdk.Key.z) || (keyval >= Gdk.Key.A && keyval <= Gdk.Key.Z) || (keyval >= Gdk.Key.@0 && keyval <= Gdk.Key.@9) || (keyval >= Gdk.Key.kana_fullstop && keyval <= Gdk.Key.semivoicedsound) || (keyval >= Gdk.Key.Arabic_comma && keyval <= Gdk.Key.Arabic_sukun) || (keyval >= Gdk.Key.Serbian_dje && keyval <= Gdk.Key.Cyrillic_HARDSIGN) || (keyval >= Gdk.Key.Greek_ALPHAaccent && keyval <= Gdk.Key.Greek_omega) || (keyval >= Gdk.Key.hebrew_doublelowline && keyval <= Gdk.Key.hebrew_taf) || (keyval >= Gdk.Key.Thai_kokai && keyval <= Gdk.Key.Thai_lekkao) || (keyval >= Gdk.Key.Hangul_Kiyeog && keyval <= Gdk.Key.Hangul_J_YeorinHieuh)) { return false; } /* Don't allow navigation keys and such */ for (var index = 0; index < FORBIDDEN_KEYVALS.length; index++) { if (keyval == FORBIDDEN_KEYVALS[index]) { return false; } } } return Gtk.accelerator_valid (keyval, modifiers); } /* This adjusts the keyval and modifiers such that it matches how * gnome-shell detects shortcuts, which works as follows: * First for the non-modifier key, the keycode that generates this * keyval at the lowest shift level is determined, which might be a * level > 0, such as for numbers in the num-row in AZERTY. * Next it checks if all the specified modifiers were pressed. */ public Ft.Accelerator normalize (uint group) { uint unmodified_keyval; uint shifted_keyval; /* We want shift to always be included as explicit modifier for * gnome-shell shortcuts. That's because users usually think of * shortcuts as including the shift key rather than being defined * for the shifted keyval. * This helps with num-row keys which have different keyvals on * different layouts for example, but also with keys that have * explicit key codes at shift level 0, that gnome-shell would prefer * over shifted ones, such the DOLLAR key. */ var explicit_modifiers = Gdk.ModifierType.SHIFT_MASK | Gtk.accelerator_get_default_mod_mask (); var used_modifiers = this.modifiers & explicit_modifiers; /* Find the base keyval of the pressed key without the explicit * modifiers. */ var display = Gdk.Display.get_default (); display.translate_key (this.keycode, this.modifiers & ~explicit_modifiers, (int) group, out unmodified_keyval, null, null, null); /* Normalize num-row keys to the number value. This allows these * shortcuts to work when switching between AZERTY and layouts where * the numbers are at shift level 0. */ display.translate_key (this.keycode, Gdk.ModifierType.SHIFT_MASK | (this.modifiers & ~explicit_modifiers), (int) group, out shifted_keyval, null, null, null); if (shifted_keyval >= Gdk.Key.@0 && shifted_keyval <= Gdk.Key.@9) { unmodified_keyval = shifted_keyval; } /* Normalise */ if (unmodified_keyval == Gdk.Key.ISO_Left_Tab) { unmodified_keyval = Gdk.Key.Tab; } /* CapsLock isn't supported as a keybinding modifier, so keep it from confusing us */ used_modifiers &= ~Gdk.ModifierType.LOCK_MASK; return Ft.Accelerator () { keycode = 0, keyval = unmodified_keyval, modifiers = used_modifiers }; } /** * Intention here is to match the behaviour somewhat of `GtkShortcutLabel` which we use * in the edit dialog. */ private string[] get_labels () { var labels = new string[0]; if (Gdk.ModifierType.SHIFT_MASK in this.modifiers) { labels += "Shift"; } if (Gdk.ModifierType.CONTROL_MASK in this.modifiers) { labels += "Ctrl"; } if (Gdk.ModifierType.META_MASK in this.modifiers) { labels += "⌘"; // aka. Command key } if (Gdk.ModifierType.ALT_MASK in this.modifiers) { labels += "Alt"; // aka. Option key / ⌥ } if (Gdk.ModifierType.SUPER_MASK in this.modifiers) { labels += "Super"; } if (Gdk.ModifierType.HYPER_MASK in this.modifiers) { labels += "Hyper"; } if (Gdk.ModifierType.META_MASK in this.modifiers) { labels += "Meta"; } var chr = (unichar) Gdk.keyval_to_unicode (this.keyval); if (chr != '\x00' && chr < '\x80' && chr.isgraph ()) { switch (chr) { case '\\': labels += "Backslash"; break; default: labels += chr.toupper ().to_string (); break; } } else { switch (this.keyval) { case Gdk.Key.Left: labels += "\xe2\x86\x90"; break; case Gdk.Key.Up: labels += "\xe2\x86\x91"; break; case Gdk.Key.Right: labels += "\xe2\x86\x92"; break; case Gdk.Key.Down: labels += "\xe2\x86\x93"; break; case Gdk.Key.space: labels += "Space"; break; case Gdk.Key.Return: labels += "Return"; break; case Gdk.Key.Page_Up: labels += "Page Up"; break; case Gdk.Key.Page_Down: labels += "Page Down"; break; default: labels += Gdk.keyval_name (this.keyval).replace ("_", " "); break; } } return labels; } public string get_label () { return string.joinv (" + ", this.get_labels ()); } public string to_string () { return !this.is_empty () ? Gtk.accelerator_name (this.keyval, this.modifiers) : ""; } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/enter-keyboard-shortcut.svg000066400000000000000000000173151520625676500335340ustar00rootroot00000000000000 preferences-panel-keyboard-shortcuts.ui000066400000000000000000000177561520625676500357500ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts preferences-panel-keyboard-shortcuts.vala000066400000000000000000000105611520625676500362410ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/keyboard-shortcuts/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/keyboard-shortcuts/preferences-panel-keyboard-shortcuts.ui")] public class PreferencesPanelKeyboardShortcuts : Ft.PreferencesPanel { [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Ft.AcceleratorRow start_stop_timer_row; [GtkChild] private unowned Ft.AcceleratorRow start_pause_resume_timer_row; [GtkChild] private unowned Ft.AcceleratorRow start_timer_row; [GtkChild] private unowned Ft.AcceleratorRow stop_timer_row; [GtkChild] private unowned Ft.AcceleratorRow pause_timer_row; [GtkChild] private unowned Ft.AcceleratorRow resume_timer_row; [GtkChild] private unowned Ft.AcceleratorRow skip_timer_row; [GtkChild] private unowned Ft.AcceleratorRow rewind_timer_row; [GtkChild] private unowned Ft.AcceleratorRow toggle_window_row; private Ft.KeyboardManager? keyboard_manager = null; construct { this.keyboard_manager = new Ft.KeyboardManager (); this.keyboard_manager.shortcut_changed.connect (this.on_shortcut_changed); this.update_accelerators (); } private void open_global_shortcuts_dialog () { get_window_identifier.begin ( this.get_root () as Gtk.Window, (obj, res) => { var window_identifier = get_window_identifier.end (res); this.keyboard_manager.open_global_shortcuts_dialog (window_identifier); }); } private void update_accelerator (string shortcut_name, string? shortcut_accelerator = null) { string accelerator = shortcut_accelerator != null ? shortcut_accelerator : this.keyboard_manager.lookup_accelerator (shortcut_name); switch (shortcut_name) { case "timer.toggle": case "timer.start-stop": this.start_stop_timer_row.accelerator = accelerator; break; case "timer.start-pause-resume": this.start_pause_resume_timer_row.accelerator = accelerator; break; case "timer.start": this.start_timer_row.accelerator = accelerator; break; case "timer.reset": this.stop_timer_row.accelerator = accelerator; break; case "timer.pause": this.pause_timer_row.accelerator = accelerator; break; case "timer.resume": this.resume_timer_row.accelerator = accelerator; break; case "session-manager.advance": this.skip_timer_row.accelerator = accelerator; break; case "timer.rewind": this.rewind_timer_row.accelerator = accelerator; break; case "app.toggle-window": this.toggle_window_row.accelerator = accelerator; break; default: GLib.warning ("Unhandled shortcut '%s'", shortcut_name); break; } } private void update_accelerators () { this.keyboard_manager.foreach_accelerator ( (shortcut_name, shortcut_accelerator) => { this.update_accelerator (shortcut_name, shortcut_accelerator); }); } private void on_shortcut_changed (string shortcut_name) { this.update_accelerator (shortcut_name); } [GtkCallback] private void on_edit_button_clicked (Gtk.Button button) { this.open_global_shortcuts_dialog (); } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { this.keyboard_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/notifications/000077500000000000000000000000001520625676500252355ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/notifications/preferences-panel-notifications.ui000066400000000000000000000050111520625676500340360ustar00rootroot00000000000000 preferences-panel-notifications.vala000066400000000000000000000141231520625676500342710ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/notifications/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/notifications/preferences-panel-notifications.ui")] public class PreferencesPanelNotifications : Ft.PreferencesPanel { private const uint[] IDLE_DELAY_CHOICES = { 15U, 30U, 60U, 120U, 180U, 300U, 0U }; [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Adw.SwitchRow announce_about_to_end_switchrow; [GtkChild] private unowned Adw.SwitchRow screen_overlay_switchrow; [GtkChild] private unowned Adw.ComboRow screen_overlay_lock_delay_comborow; [GtkChild] private unowned Adw.ComboRow screen_overlay_reopen_delay_comborow; private GLib.Settings? settings; private Ft.IdleMonitor? idle_monitor = null; construct { this.settings = Ft.get_settings (); this.settings.changed.connect (this.on_settings_changed); this.idle_monitor = new Ft.IdleMonitor (); this.screen_overlay_lock_delay_comborow.model = this.create_idle_delay_model (); this.screen_overlay_reopen_delay_comborow.model = this.create_idle_delay_model (); // Announcements this.settings.bind ("announce-about-to-end", this.announce_about_to_end_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); // Screen Overlay this.settings.bind ("screen-overlay", this.screen_overlay_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); this.settings.bind_with_mapping ( "screen-overlay-lock-delay", this.screen_overlay_lock_delay_comborow, "selected", GLib.SettingsBindFlags.DEFAULT, idle_delay_get_mapping, idle_delay_set_mapping, null, null); this.settings.bind_with_mapping ( "screen-overlay-reopen-delay", this.screen_overlay_reopen_delay_comborow, "selected", GLib.SettingsBindFlags.DEFAULT, idle_delay_get_mapping, idle_delay_set_mapping, null, null); this.idle_monitor.bind_property ( "enabled", this.screen_overlay_lock_delay_comborow, "visible", GLib.BindingFlags.SYNC_CREATE); this.idle_monitor.bind_property ( "enabled", this.screen_overlay_reopen_delay_comborow, "visible", GLib.BindingFlags.SYNC_CREATE); this.screen_overlay_switchrow.bind_property ( "active", this.screen_overlay_lock_delay_comborow, "sensitive", GLib.BindingFlags.SYNC_CREATE); this.screen_overlay_switchrow.bind_property ( "active", this.screen_overlay_reopen_delay_comborow, "sensitive", GLib.BindingFlags.SYNC_CREATE); } private Gtk.StringList create_idle_delay_model () { var list = new Gtk.StringList (null); foreach (var seconds in IDLE_DELAY_CHOICES) { var label = seconds != 0 ? Ft.format_time (seconds) : _("Never"); list.append (label); } return list; } /** * Convert settings value to a choice. */ private static bool idle_delay_get_mapping (GLib.Value value, GLib.Variant variant, void* user_data) { var seconds = variant.get_uint32 (); for (var choice_index = 0U; choice_index < IDLE_DELAY_CHOICES.length; choice_index++) { if (seconds == IDLE_DELAY_CHOICES[choice_index]) { value.set_uint (choice_index); return true; } } GLib.warning ("Could not map idle_delay to a choice"); value.set_uint (30); return true; } /** * Convert choice to settings value. */ private static GLib.Variant idle_delay_set_mapping (GLib.Value value, GLib.VariantType expected_type, void* user_data) { var choice_index = value.get_uint (); var choice_value = IDLE_DELAY_CHOICES[choice_index]; return new GLib.Variant.uint32 (choice_value); } private void on_settings_changed (GLib.Settings settings, string key) { } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { if (this.settings != null) { this.settings.changed.disconnect (this.on_settings_changed); this.settings = null; } this.idle_monitor = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/preferences-window.ui000066400000000000000000000041131520625676500265300ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/preferences-window.vala000066400000000000000000000213321520625676500270400ustar00rootroot00000000000000/* * Copyright (c) 2013,2014,2016,2024 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ using GLib; namespace Ft { private class PreferencesPanelInfo : GLib.Object { public string name { get; set; } public string title { get; set; } public string icon_name { get; set; } public GLib.Type content_class { get; set; } public bool visible { get; set; default = true; } } private Gtk.SingleSelection create_model () { Ft.PreferencesPanelInfo? panel_info; var model = new GLib.ListStore (typeof (Ft.PreferencesPanelInfo)); panel_info = new PreferencesPanelInfo (); panel_info.name = "timer"; panel_info.title = _("Timer"); panel_info.icon_name = "timer-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelTimer); model.append (panel_info); panel_info = new PreferencesPanelInfo (); panel_info.name = "notifications"; panel_info.title = _("Notifications"); panel_info.icon_name = "preferences-notifications-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelNotifications); model.append (panel_info); panel_info = new PreferencesPanelInfo (); panel_info.name = "sounds"; panel_info.title = _("Sounds"); panel_info.icon_name = "preferences-sounds-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelSounds); model.append (panel_info); panel_info = new PreferencesPanelInfo (); panel_info.name = "appearance"; panel_info.title = _("Appearance"); panel_info.icon_name = "preferences-appearance-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelAppearance); model.append (panel_info); panel_info = new PreferencesPanelInfo (); panel_info.name = "keyboard-shortcuts"; panel_info.title = _("Keyboard Shortcuts"); panel_info.icon_name = "preferences-keyboard-shortcuts-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelKeyboardShortcuts); model.append (panel_info); var keyboard_manager = new Ft.KeyboardManager (); keyboard_manager.bind_property ("global-shortcuts-supported", panel_info, "visible", GLib.BindingFlags.SYNC_CREATE); panel_info = new PreferencesPanelInfo (); panel_info.name = "integrations"; panel_info.title = _("Integrations"); panel_info.icon_name = "plugin-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelIntegrations); model.append (panel_info); #if ENABLE_AUTOMATION panel_info = new PreferencesPanelInfo (); panel_info.name = "automation"; panel_info.title = _("Automation"); panel_info.icon_name = "custom-action-symbolic"; panel_info.content_class = typeof (Ft.PreferencesPanelAutomation); model.append (panel_info); #endif return new Gtk.SingleSelection ((owned) model); } public abstract class PreferencesPanel : Adw.NavigationPage { public abstract unowned Adw.PreferencesPage get_preferences_page (); } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/preferences-window.ui")] public class PreferencesWindow : Adw.ApplicationWindow, Gtk.Buildable { public Gtk.SingleSelection? model { get { return this._model; } construct { this._model = create_model (); this._model.selection_changed.connect (this.on_selection_changed); } } public Ft.PreferencesPanel? visible_panel { get { return this.split_view.content as Ft.PreferencesPanel; } } [GtkChild] private unowned Adw.ToastOverlay toast_overlay; [GtkChild] private unowned Adw.NavigationSplitView split_view; [GtkChild] private unowned Ft.PreferencesSidebar sidebar; private Gtk.SingleSelection? _model = null; private GLib.Settings? settings = null; private Peas.ExtensionSet? extensions = null; construct { this.settings = Ft.get_settings (); this.sidebar.model = this._model; this.split_view.notify["collapsed"].connect (this.on_split_view_collapsed_notify); this.load_window_state (); this.update_split_view_content (); this.extensions = new Peas.ExtensionSet.with_properties ( Peas.Engine.get_default (), typeof (Ft.PreferencesWindowExtension), {}, {}); this.extensions.extension_added.connect (this.on_extension_added); foreach_extension (this.extensions, (object) => { this.setup_extension (object); }); } private void load_window_state () { var current_width = -1; var current_height = -1; var maximized = false; this.settings.@get ("preferences-window-state", "(iib)", out current_width, out current_height, out maximized); if (current_width != -1 && current_height != -1) { this.set_default_size (current_width, current_height); } if (maximized) { this.maximize (); } } private void save_window_state () { var current_width = -1; var current_height = -1; var maximized = this.is_maximized (); this.get_default_size (out current_width, out current_height); this.settings.@set ("preferences-window-state", "(iib)", current_width, current_height, maximized); } private void update_split_view_content () { var panel_info = (Ft.PreferencesPanelInfo?) this._model.selected_item; if (panel_info != null) { var panel = (Ft.PreferencesPanel) GLib.Object.@new ( panel_info.content_class, tag: panel_info.name, title: panel_info.title); this.split_view.content = panel; this.split_view.show_content = true; this.notify_property ("visible-panel"); } else { this.split_view.show_content = false; } } private void setup_extension (GLib.Object object) { var extension = (Ft.PreferencesWindowExtension) object; extension.window = this; } private void on_split_view_collapsed_notify () { this.sidebar.selection_mode = this.split_view.collapsed ? Gtk.SelectionMode.NONE : Gtk.SelectionMode.SINGLE; } private void on_selection_changed (uint position, uint n_items) { this.update_split_view_content (); } private void on_extension_added (Peas.ExtensionSet extension_set, Peas.PluginInfo plugin_info, GLib.Object object) { this.setup_extension (object); } public bool select_panel (string panel_name) { var n_items = this._model.get_n_items (); for (var position = 0U; position < n_items; position++) { var panel_info = (Ft.PreferencesPanelInfo?) this._model.get_item (position); if (panel_info.name == panel_name) { this._model.select_item (position, true); return true; } } return false; } public void add_toast (owned Adw.Toast toast) { this.toast_overlay.add_toast (toast); } public override void unmap () { this.save_window_state (); base.unmap (); } public override void dispose () { if (this._model != null) { this._model.selection_changed.disconnect (this.on_selection_changed); this._model = null; } this.extensions = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/000077500000000000000000000000001520625676500236775ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/preferences-panel-sounds.ui000066400000000000000000000147151520625676500311550ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/preferences-panel-sounds.vala000066400000000000000000000160161520625676500314570ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { private struct Preset { public string uri; public string label; } [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/sounds/preferences-panel-sounds.ui")] public class PreferencesPanelSounds : Ft.PreferencesPanel { private const Preset[] ALERT_PRESETS = { { "bell.ogg", N_("Bell") }, { "loud-bell.ogg", N_("Loud Bell") }, }; private const Preset[] BACKGROUND_PRESETS = { { "clock.ogg", N_("Clock Ticking") }, { "metronome.ogg", N_("Metronome") }, { "brown-noise.ogg", N_("Brown Noise") }, }; [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Gtk.Switch enable_switch; [GtkChild] private unowned Gtk.Stack stack; [GtkChild] private unowned Gtk.Label pomodoro_finished_sound_label; [GtkChild] private unowned Gtk.Label break_finished_sound_label; [GtkChild] private unowned Gtk.Label background_sound_label; private GLib.Settings? settings = null; private ulong settings_changed_id = 0; construct { this.settings = Ft.get_settings (); this.settings.bind ("sounds", this.enable_switch, "active", GLib.SettingsBindFlags.DEFAULT); this.settings_changed_id = this.settings.changed.connect (this.on_settings_changed); this.enable_switch.bind_property ("active", this.stack, "visible-child-name", GLib.BindingFlags.SYNC_CREATE, transform_active_to_visible_child_name); this.update_sound_labels (); } private static bool transform_active_to_visible_child_name (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { target_value.set_string (source_value.get_boolean () ? "enabled" : "disabled"); return true; } private Ft.SoundChooserWindow create_sound_chooser (string title, string? event_id, Preset[] presets) { var chooser = new Ft.SoundChooserWindow (); chooser.title = title; chooser.event_id = event_id; chooser.transient_for = (Gtk.Window) this.get_root (); for (var index = 0; index < presets.length; index++) { chooser.add_preset (presets[index].uri, gettext (presets[index].label)); } return chooser; } private string format_sound_label (string uri) { if (uri == "" || uri == null) { return _("None"); } foreach (var preset in ALERT_PRESETS) { if (preset.uri == uri) { return gettext (preset.label); } } foreach (var preset in BACKGROUND_PRESETS) { if (preset.uri == uri) { return gettext (preset.label); } } return GLib.File.new_for_uri (uri).get_basename (); } private void update_sound_labels () { this.pomodoro_finished_sound_label.label = this.format_sound_label ( this.settings.get_string ("pomodoro-finished-sound")); this.break_finished_sound_label.label = this.format_sound_label ( this.settings.get_string ("break-finished-sound")); this.background_sound_label.label = this.format_sound_label ( this.settings.get_string ("background-sound")); } [GtkCallback] private void on_pomodoro_finished_sound_activated (Adw.ActionRow action_row) { var chooser = this.create_sound_chooser (action_row.title, "pomodoro-finished", ALERT_PRESETS); this.settings.bind ("pomodoro-finished-sound", chooser, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("pomodoro-finished-sound-volume", chooser, "volume", GLib.SettingsBindFlags.DEFAULT); chooser.present (); } [GtkCallback] private void on_break_finished_sound_activated (Adw.ActionRow action_row) { var chooser = this.create_sound_chooser (action_row.title, "break-finished", ALERT_PRESETS); this.settings.bind ("break-finished-sound", chooser, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("break-finished-sound-volume", chooser, "volume", GLib.SettingsBindFlags.DEFAULT); chooser.present (); } [GtkCallback] private void on_background_sound_activated (Adw.ActionRow action_row) { var chooser = this.create_sound_chooser (action_row.title, null, BACKGROUND_PRESETS); this.settings.bind ("background-sound", chooser, "uri", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("background-sound-volume", chooser, "volume", GLib.SettingsBindFlags.DEFAULT); chooser.present (); } private void on_settings_changed (GLib.Settings settings, string key) { switch (key) { case "pomodoro-finished-sound": case "break-finished-sound": case "background-sound": this.update_sound_labels (); break; default: break; } } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { if (this.settings_changed_id != 0) { this.settings.disconnect (this.settings_changed_id); this.settings_changed_id = 0; } this.settings = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/sound-chooser-window.ui000066400000000000000000000075111520625676500303370ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/sound-chooser-window.vala000066400000000000000000000266011520625676500306460ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/sounds/sound-chooser-window.ui")] public class SoundChooserWindow : Adw.Window { [CCode (notify = false)] public string uri { get { return this._uri; } set { if (this._uri != value) { this._uri = value; this.update_active_row (); this.notify_property ("uri"); } } } [CCode (notify = false)] public string event_id { get { return this._event_id; } set { if (this._event_id != value) { this._event_id = value; this.destroy_sound (); this.ensure_sound (); this.notify_property ("event-id"); } } } public double volume { get; set; default = 1.0; } [GtkChild] private unowned Adw.ToastOverlay toast_overlay; [GtkChild] private unowned Adw.PreferencesRow none_row; [GtkChild] private unowned Adw.PreferencesRow add_row; [GtkChild] private unowned Gtk.CheckButton radio_group; [GtkChild] private unowned Ft.VolumeSlider volume_slider; private string _uri = ""; private string _event_id = ""; private unowned Gtk.ListBox? list_box = null; private Ft.Sound? sound = null; private Ft.SoundManager? sound_manager = null; private Adw.Toast? toast = null; private int next_position = 1; private GLib.Cancellable? cancellable = null; construct { this.sound_manager = new Ft.SoundManager (); this.none_row.set_data ("uri", ""); this.list_box = (Gtk.ListBox) this.none_row.parent; this.list_box.activate_on_single_click = true; this.list_box.row_activated.connect (this.on_row_activated); this.bind_property ("volume", this.volume_slider, "value", GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL); this.update_volume_slider_sensitivity (); } private void ensure_sound () { if (this.sound != null) { return; } if (this._event_id != "" && this._event_id != null) { this.sound = new Ft.AlertSound (this._event_id); } else { this.sound = new Ft.BackgroundSound (); } this.sound.notify["error"].connect (this.on_sound_error); this.bind_property ("volume", this.sound, "volume", GLib.BindingFlags.SYNC_CREATE); } private void destroy_sound () { if (this.sound != null) { this.sound.notify["error"].disconnect (this.on_sound_error); this.sound.stop (); this.sound = null; } } private static inline string get_row_uri (Gtk.Widget row) { return row.get_data ("uri"); } private static inline void set_row_uri (Gtk.Widget row, string uri) { row.set_data ("uri", uri); } private unowned Gtk.Widget? get_row_by_uri (string uri) { unowned var row = (Gtk.Widget) this.none_row; while (row != null) { if (get_row_uri (row) == uri) { return row; } row = row.get_next_sibling (); } return null; } private unowned Gtk.Widget? get_row_by_widget (Gtk.Widget widget) { unowned var row = widget.parent; while (row != null) { if (row is Adw.ActionRow) { return row; } row = row.parent; } return null; } private void update_active_radio () { for (var index = 0; index < this.next_position; index++) { var row = (Adw.ActionRow) this.list_box.get_row_at_index (index); var radio = (Gtk.CheckButton) row.activatable_widget; radio.active = get_row_uri (row) == this._uri; } } private void update_volume_slider_sensitivity () { this.volume_slider.parent.sensitive = this._uri != ""; } private void update_active_row () { var existing_row = this.get_row_by_uri (this._uri); if (existing_row == null) { var label = GLib.File.new_for_uri (this._uri).get_basename (); this.add_preset_internal (this._uri, label, true); } else { this.update_active_radio (); } this.update_volume_slider_sensitivity (); if (this.sound != null) { this.sound.stop (); } } private void add_preset_internal (string uri, string label, bool removable = false) { var radio = new Gtk.CheckButton (); radio.valign = Gtk.Align.CENTER; radio.group = this.radio_group; radio.active = this._uri == uri; radio.toggled.connect (this.on_radio_toggled); var row = new Adw.ActionRow (); row.use_markup = false; row.title = label; row.activatable = true; row.activatable_widget = radio; set_row_uri (row, uri); if (removable) { var remove_button = new Gtk.Button (); remove_button.icon_name = "window-close-symbolic"; remove_button.valign = Gtk.Align.CENTER; remove_button.add_css_class ("flat"); remove_button.clicked.connect (this.on_remove_button_clicked); row.add_suffix (remove_button); } row.add_suffix (radio); this.list_box.insert (row, this.next_position); this.next_position++; } private void open_file_chooser () { this.ensure_sound (); if (this.cancellable != null) { this.cancellable.cancel (); } this.cancellable = new GLib.Cancellable (); var file_filter = new Gtk.FileFilter (); foreach (var mime_type in Ft.SoundPlayer.get_supported_mime_types ()) { file_filter.add_mime_type (mime_type); } var file_dialog = new Gtk.FileDialog (); file_dialog.title = _("Select Custom Sound"); file_dialog.modal = true; file_dialog.accept_label = _("_Select"); file_dialog.default_filter = file_filter; file_dialog.open.begin ( this, this.cancellable, (obj, res) => { GLib.File? file = null; try { file = file_dialog.open.end (res); } catch (GLib.Error error) { if (this.toast != null) { this.toast.dismiss (); } return; } var uri = file != null ? file.get_uri () : ""; var existing_row = this.get_row_by_uri (uri); if (existing_row == null && file != null) { this.add_preset_internal (uri, file.get_basename (), true); } this.uri = uri; }); } private void preview () { if (this.toast != null) { this.toast.dismiss (); this.toast = null; } this.ensure_sound (); this.sound.uri = this._uri; if (this.sound.error == null) { this.sound.play (); } } private void on_row_activated (Gtk.ListBox listbox, Gtk.ListBoxRow row) { if (row == this.add_row) { this.open_file_chooser (); } else { this.uri = get_row_uri (row); this.preview (); } } private void on_sound_error () { var error = this.sound.error; if (this.toast != null) { this.toast.dismiss (); this.toast = null; } if (error != null) { this.toast = new Adw.Toast (error.message); this.toast_overlay.add_toast (this.toast); } } private void on_remove_button_clicked (Gtk.Button button) { var row = this.get_row_by_widget (button); this.remove_preset (get_row_uri (row)); } [GtkCallback] private void on_radio_toggled (Gtk.CheckButton radio) { if (radio.active) { this.uri = get_row_uri (this.get_row_by_widget (radio)); } } [GtkCallback] private bool on_key_pressed (Gtk.EventControllerKey event_controller, uint keyval, uint keycode, Gdk.ModifierType state) { switch (keyval) { case Gdk.Key.Escape: this.close (); return true; } return false; } public void add_preset (string uri, string label) { this.add_preset_internal (uri, label); } public void remove_preset (string uri) { var row = this.get_row_by_uri (uri); if (row != null) { this.list_box.remove (row); this.next_position--; } if (this._uri == uri) { this.uri = ""; } } public override void map () { base.map (); this.sound_manager.inhibit_background_sound (); } public override void unmap () { base.unmap (); this.destroy_sound (); this.sound_manager.uninhibit_background_sound (); } public override void dispose () { if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } this.list_box = null; this.sound_manager = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/volume-slider.ui000066400000000000000000000025421520625676500270300ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/sounds/volume-slider.vala000066400000000000000000000041041520625676500273320ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/sounds/volume-slider.ui")] public class VolumeSlider : Gtk.Box { private const string[] VOLUME_IMAGES = { "audio-volume-muted-symbolic", "audio-volume-low-symbolic", "audio-volume-medium-symbolic", "audio-volume-high-symbolic" }; [CCode (notify = false)] public double value { get { return this.adjustment.value; } set { this.adjustment.value = value; } } [GtkChild] private unowned Gtk.Image volume_image; [GtkChild] private unowned Gtk.Adjustment adjustment; private double last_volume = 1.0; construct { this.update_volume_image (); } private void update_volume_image () { var value = this.adjustment.value; string icon_name; if (value == 0.0) { icon_name = VOLUME_IMAGES[0]; } else if (value < 0.3) { icon_name = VOLUME_IMAGES[1]; } else if (value < 0.7) { icon_name = VOLUME_IMAGES[2]; } else { icon_name = VOLUME_IMAGES[3]; } this.volume_image.icon_name = icon_name; } [GtkCallback] private void on_mute_button_clicked () { if (this.adjustment.value > 0.0) { this.last_volume = this.adjustment.value; this.adjustment.value = 0.0; } else { this.adjustment.value = this.last_volume > 0.0 ? this.last_volume : 1.0; } } [GtkCallback] private void on_adjustment_value_changed () { this.update_volume_image (); this.notify_property ("value"); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/000077500000000000000000000000001520625676500235045ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/preferences-panel-timer.ui000066400000000000000000000123201520625676500305550ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/preferences-panel-timer.vala000066400000000000000000000227361520625676500310770ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/timer/preferences-panel-timer.ui")] public class PreferencesPanelTimer : Ft.PreferencesPanel { private const uint MIN_TOAST_TIMEOUT = 3; private const uint MAX_TOAST_TIMEOUT = 30; [GtkChild] private unowned Adw.PreferencesPage page; [GtkChild] private unowned Gtk.Adjustment pomodoro_duration_adjustment; [GtkChild] private unowned Gtk.Adjustment short_break_duration_adjustment; [GtkChild] private unowned Gtk.Adjustment long_break_duration_adjustment; [GtkChild] private unowned Gtk.Adjustment cycles_adjustment; [GtkChild] private unowned Gtk.Label session_stats_label; [GtkChild] private unowned Gtk.Label breaks_stats_label; [GtkChild] private unowned Adw.SwitchRow pause_on_lockscreen_switchrow; [GtkChild] private unowned Adw.SwitchRow confirm_starting_break_switchrow; [GtkChild] private unowned Adw.SwitchRow confirm_starting_pomodoro_switchrow; [GtkChild] private unowned Ft.LogScaleRow long_break_row; private GLib.Settings? settings; private ulong settings_changed_id = 0; private Ft.Timer? timer; private ulong timer_state_changed_id = 0; private Ft.IdleMonitor? idle_monitor; private Adw.Toast? apply_changes_toast; construct { this.settings = Ft.get_settings (); this.settings.bind ("pomodoro-duration", this.pomodoro_duration_adjustment, "value", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("short-break-duration", this.short_break_duration_adjustment, "value", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("long-break-duration", this.long_break_duration_adjustment, "value", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("cycles", this.cycles_adjustment, "value", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("pause-on-lockscreen", this.pause_on_lockscreen_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("confirm-starting-break", this.confirm_starting_break_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); this.settings.bind ("confirm-starting-pomodoro", this.confirm_starting_pomodoro_switchrow, "active", GLib.SettingsBindFlags.DEFAULT); this.idle_monitor = new Ft.IdleMonitor (); this.idle_monitor.bind_property ("enabled", this.confirm_starting_pomodoro_switchrow, "visible", GLib.BindingFlags.SYNC_CREATE); this.settings_changed_id = settings.changed.connect (this.on_settings_changed); this.timer = Ft.Timer.get_default (); if (this.timer_state_changed_id == 0) { this.timer_state_changed_id = this.timer.state_changed.connect (this.on_timer_state_changed); } this.update_long_break_row_sensitivity (); this.update_stats_labels (); } private void update_long_break_row_sensitivity () { this.long_break_row.sensitive = this.cycles_adjustment.value > 1.0; } private void update_stats_labels () { var session_template = Ft.SessionTemplate.with_defaults (); var total_duration = Ft.Timestamp.to_seconds_uint (session_template.calculate_total_duration ()); var break_percentage = (uint) Math.round (session_template.calculate_break_percentage ()); // translators: time formatted as text: "5 minutes 30 seconds" this.session_stats_label.label = _("A single session will take %s.").printf (Ft.format_time (total_duration)); this.breaks_stats_label.label = _("%u%% of the time will be allocated for breaks.").printf (break_percentage); } private void apply_changes () { var current_time_block = this.timer.user_data as Ft.TimeBlock; if (current_time_block != null) { var duration = current_time_block.state.get_default_duration (); current_time_block.set_intended_duration (duration); this.timer.duration = duration; } } private uint calculate_apply_changes_toast_timeout (Ft.State changed_state) { var current_time_block = this.timer.user_data as Ft.TimeBlock; if (current_time_block == null || current_time_block.state != changed_state || current_time_block.get_intended_duration () == current_time_block.state.get_default_duration ()) { return 0U; } if (this.timer.is_paused ()) { return MAX_TOAST_TIMEOUT; } var timeout = Ft.Timestamp.to_seconds_uint ( current_time_block.state.get_default_duration () - this.timer.calculate_elapsed ()); return uint.min (timeout, MAX_TOAST_TIMEOUT); } private void show_apply_changes_toast (Ft.State changed_state, uint timeout) { var window = this.get_root () as Ft.PreferencesWindow; var toast = this.apply_changes_toast; if (toast == null) { toast = new Adw.Toast (changed_state == Ft.State.POMODORO ? _("Apply changes to ongoing Pomodoro?") : _("Apply changes to ongoing break?")); toast.use_markup = false; toast.button_label = _("Apply"); toast.button_clicked.connect ( () => { this.apply_changes_toast = null; this.apply_changes (); } ); toast.dismissed.connect (() => { this.apply_changes_toast = null; }); this.apply_changes_toast = toast; } toast.timeout = timeout; window?.add_toast (toast); } private void hide_apply_changes_toast () { if (this.apply_changes_toast != null) { this.apply_changes_toast.dismiss (); } } private void on_timer_state_changed (Ft.TimerState current_state, Ft.TimerState previous_state) { this.hide_apply_changes_toast (); } private void on_settings_changed (GLib.Settings settings, string key) { var changed_state = Ft.State.STOPPED; var session_manager = Ft.SessionManager.get_default (); switch (key) { case "pomodoro-duration": changed_state = Ft.State.POMODORO; this.update_stats_labels (); break; case "short-break-duration": changed_state = settings.get_uint ("cycles") > 1 ? Ft.State.SHORT_BREAK : Ft.State.BREAK; this.update_stats_labels (); break; case "long-break-duration": changed_state = Ft.State.LONG_BREAK; this.update_stats_labels (); break; case "cycles": this.update_long_break_row_sensitivity (); this.update_stats_labels (); break; default: break; } if (session_manager.current_time_block != null && session_manager.current_time_block.state == changed_state) { var apply_changes_toast_timeout = this.calculate_apply_changes_toast_timeout (changed_state); if (apply_changes_toast_timeout >= MIN_TOAST_TIMEOUT) { this.show_apply_changes_toast (changed_state, apply_changes_toast_timeout); } else { this.hide_apply_changes_toast (); } } } public override unowned Adw.PreferencesPage get_preferences_page () { return this.page; } public override void dispose () { this.hide_apply_changes_toast (); if (this.settings_changed_id != 0) { this.settings.disconnect (this.settings_changed_id); this.settings_changed_id = 0; } if (this.timer_state_changed_id != 0) { this.timer.disconnect (this.timer_state_changed_id); this.timer_state_changed_id = 0; } this.settings = null; this.timer = null; this.idle_monitor = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/widgets/000077500000000000000000000000001520625676500251525ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/widgets/log-scale-row.ui000066400000000000000000000034661520625676500301750ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/widgets/log-scale-row.vala000066400000000000000000000042271520625676500304770ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { // TODO: rename to DurationRow [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/preferences/timer/widgets/log-scale-row.ui")] public class LogScaleRow : Adw.ActionRow { public Gtk.Adjustment adjustment { get { return this._adjustment; } set { if (this._adjustment != null) { this._adjustment.disconnect (this.value_changed_id); this.value_changed_id = 0; } this._adjustment = value; if (this._adjustment != null) { this.value_changed_id = this._adjustment.value_changed.connect (this.on_value_changed); } } } [GtkChild] private unowned Gtk.Label title_label; [GtkChild] private unowned Gtk.Label value_label; [GtkChild] private unowned Ft.LogScale scale; private Gtk.Adjustment _adjustment; private ulong value_changed_id = 0; construct { this.bind_property ("title", this.title_label, "label", GLib.BindingFlags.SYNC_CREATE); this.bind_property ("adjustment", this.scale, "adjustment", GLib.BindingFlags.SYNC_CREATE); this.update_value_label (); } private void update_value_label () { if (this._adjustment != null) { var seconds = (int) Math.round (this._adjustment.value).clamp (0, int.MAX); this.value_label.label = Ft.format_time (seconds); } } private void on_value_changed () { this.update_value_label (); } public override void dispose () { if (this._adjustment != null) { this._adjustment.disconnect (this.value_changed_id); this.value_changed_id = 0; } this._adjustment = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/timer/widgets/log-scale.vala000066400000000000000000000106771520625676500277000ustar00rootroot00000000000000/* * Copyright (c) 2013-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public class LogScale : Gtk.Scale { private const GLib.BindingFlags BINDING_FLAGS = GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE | GLib.BindingFlags.BIDIRECTIONAL; public double value { get; set; } public new Gtk.Adjustment adjustment { get { return this._adjustment; } set { if (this.value_binding != null) { this.value_binding.unbind (); this.value_binding = null; } this._adjustment = value; if (this._adjustment != null) { this.value_binding = this._adjustment.bind_property ("value", base.adjustment, "value", BINDING_FLAGS, this.transform_to, this.transform_from); } } } private Gtk.Adjustment? _adjustment; private unowned GLib.Binding? value_binding; construct { base.adjustment = new Gtk.Adjustment (0.0, 0.0, 2.0, 0.0, 0.0, 0.0); } public LogScale () { GLib.Object ( orientation: Gtk.Orientation.HORIZONTAL, digits: -1, draw_value: false ); } /** * Round seconds to 30s, 1m, 5m, 10m. * * Its intended for settings only to have a rounded number. */ private double round_seconds (double seconds) { if (seconds < 60.0) { return 30.0 * Math.round (seconds / 30.0); } if (seconds < 1800.0) { return 60.0 * Math.round (seconds / 60.0); } if (seconds < 3600.0) { return 300.0 * Math.round (seconds / 300.0); } return 600.0 * Math.round (seconds / 600.0); } private inline double func (double x) { return Math.exp (x) - 1.0; } private inline double func_inv (double y) { return Math.log (y + 1.0); } /** * Convert inner adjustment to destination values (seconds). */ private bool transform_from (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var seconds_lower = this._adjustment.lower; var seconds_upper = this._adjustment.upper; var base_upper = base.adjustment.upper; var base_value = source_value.get_double (); var t = this.func (base_value) / this.func (base_upper); var seconds = t * (seconds_upper - seconds_lower) + seconds_lower; target_value.set_double (this.round_seconds (seconds)); return true; } /** * Convert outer adjustment (seconds) to inner adjustment. */ private bool transform_to (GLib.Binding binding, GLib.Value source_value, ref GLib.Value target_value) { var seconds_lower = this._adjustment.lower; var seconds_upper = this._adjustment.upper; var base_upper = base.adjustment.upper; var seconds = source_value.get_double (); var t = (seconds - seconds_lower) / (seconds_upper - seconds_lower); var base_value = this.func_inv (t * this.func (base_upper)); target_value.set_double (base_value); return true; } public override void dispose () { if (this.value_binding != null) { this.value_binding.unbind (); } base.dispose (); this._adjustment = null; } } } focustimerhq-FocusTimer-8581be2/src/ui/preferences/widgets/000077500000000000000000000000001520625676500240325ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/preferences/widgets/preferences-sidebar.vala000066400000000000000000000144661520625676500306220ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public class PreferencesSidebar : Gtk.Widget { public Gtk.SingleSelection model { get { return this._model; } set { if (this._model == value) { return; } if (this._model != null) { this._model.items_changed.disconnect (this.on_model_items_changed); this._model.selection_changed.disconnect (this.on_selection_changed); } this.clear (); this._model = value; this.populate (); if (this._model != null) { this._model.items_changed.connect (this.on_model_items_changed); this._model.selection_changed.connect (this.on_selection_changed); } this.notify_property ("model"); } } public Gtk.SelectionMode selection_mode { get { return this.list.selection_mode; } set { this.list.selection_mode = value; if (this.list.selection_mode != Gtk.SelectionMode.NONE) { var active_row = this.list.get_row_at_index ((int) this._model.selected); this.list.select_row (active_row); } } } private Gtk.SingleSelection? _model; private Gtk.ListBox? list; private ulong row_selected_id; construct { this.layout_manager = new Gtk.BinLayout (); var scrolled_window = new Gtk.ScrolledWindow (); scrolled_window.set_policy (Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); scrolled_window.set_parent (this); this.list = new Gtk.ListBox (); this.list.add_css_class ("navigation-sidebar"); this.list.update_property (Gtk.AccessibleProperty.LABEL, C_("accessibility", "Sidebar"), -1); scrolled_window.set_child (this.list); this.row_selected_id = this.list.row_selected.connect (this.on_row_selected); this.list.row_activated.connect (this.on_row_activated); this.add_css_class ("sidebar"); } private void add_row (uint position) { var icon = new Gtk.Image (); var label = new Gtk.Label (""); label.halign = Gtk.Align.START; label.valign = Gtk.Align.CENTER; var hbox = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12); hbox.margin_start = 6; hbox.margin_end = 6; hbox.margin_top = 12; hbox.margin_bottom = 12; hbox.append (icon); hbox.append (label); var row = new Gtk.ListBoxRow (); row.child = hbox; row.update_relation (Gtk.AccessibleRelation.LABELLED_BY, label, null, -1); row.set_data ("child-index", position); var panel_info = this._model.get_item (position); panel_info.bind_property ("title", label, "label", GLib.BindingFlags.SYNC_CREATE); panel_info.bind_property ("icon-name", icon, "icon-name", GLib.BindingFlags.SYNC_CREATE); panel_info.bind_property ("visible", row, "visible", GLib.BindingFlags.SYNC_CREATE); this.list.append (row); if (this._model.is_selected (position)) { this.list.select_row (row); } else { this.list.unselect_row (row); } } private void clear () { this.list.remove_all (); } private void populate () { var n_items = this._model.get_n_items (); for (var position = 0; position < n_items; position++) { this.add_row (position); } } private void on_row_selected (Gtk.ListBoxRow? row) { if (row == null) { return; } var position = row.get_data ("child-index"); this._model.select_item (position, true); } private void on_row_activated (Gtk.ListBoxRow? row) { var position = row.get_data ("child-index"); if (this.selection_mode == Gtk.SelectionMode.NONE) { this._model.set_selected (position); } } private void on_model_items_changed (uint position, uint removed, uint added) { this.clear (); this.populate (); } private void on_selection_changed (uint position, uint n_items) { GLib.SignalHandler.block (this.list, this.row_selected_id); for (var i = position; i < position + n_items; i++) { var row = this.list.get_row_at_index ((int) i); if (row != null) { if (this._model.is_selected (i)) { this.list.select_row (row); } else { this.list.unselect_row (row); } } } GLib.SignalHandler.unblock (this.list, this.row_selected_id); } public override void dispose () { if (this._model != null) { this._model.items_changed.disconnect (this.on_model_items_changed); this._model.selection_changed.disconnect (this.on_selection_changed); this._model = null; } if (this.list != null) { this.clear (); this.list.row_selected.disconnect (this.on_row_selected); this.list.row_activated.disconnect (this.on_row_activated); this.list = null; } var child = this.get_first_child (); if (child != null) { child.unparent (); } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/screen-overlay-provider.vala000066400000000000000000000042731520625676500255240ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Ft { public class DefaultScreenOverlayProvider : Ft.Provider, Ft.ScreenOverlayProvider { private GLib.Cancellable? cancellable = null; construct { this.available = true; } public void open () { if (this.cancellable != null && !this.cancellable.is_cancelled ()) { return; } var screen_overlay_group = new Ft.LightboxGroup (typeof (Ft.ScreenOverlay)); var cancellable = new GLib.Cancellable (); screen_overlay_group.open.begin ( cancellable, (obj, res) => { try { screen_overlay_group.open.end (res); } catch (GLib.Error error) { if (!cancellable.is_cancelled ()) { GLib.warning ("Failed to open overlay: %s", error.message); cancellable.cancel (); } } if (this.cancellable == cancellable) { this.cancellable = null; this.closed (); } }); if (!cancellable.is_cancelled ()) { this.cancellable = cancellable; this.opened (); } } public void close () { if (this.cancellable != null) { this.cancellable.cancel (); this.cancellable = null; } } protected override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { } protected override async void uninitialize () throws GLib.Error { } protected override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { } protected override async void disable () throws GLib.Error { this.close (); } } } focustimerhq-FocusTimer-8581be2/src/ui/screen-saver-provider.vala000066400000000000000000000031371520625676500251610ustar00rootroot00000000000000/* * Copyright (c) 2026 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { private class DefaultScreenSaverProvider : Ft.Provider, Ft.ScreenSaverProvider { public bool active { get { return this._active; } } private bool _active = false; private Gtk.Application? application = null; private void on_notify_screensaver_active (GLib.Object obj, GLib.ParamSpec pspec) { var active = this.application.screensaver_active; if (this._active != active) { this._active = active; this.notify_property ("active"); } } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.available = true; } public override async void uninitialize () throws GLib.Error { } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.application = (Gtk.Application) Ft.Application.get_default (); this.application.notify["screensaver-active"].connect (this.on_notify_screensaver_active); } public override async void disable () throws GLib.Error { if (this.application != null) { this.application.notify["screensaver-active"].disconnect (this.on_notify_screensaver_active); this.application = null; } } } } focustimerhq-FocusTimer-8581be2/src/ui/style.css000066400000000000000000000267031520625676500217450ustar00rootroot00000000000000/* * Colors * * Most of custom widgets require that colors are opaque, so we can't rely on * theme variables unfortunately. */ @define-color ft_primary_color color-mix(in srgb, @window_fg_color 97.5%, @window_bg_color); @define-color ft_unfocused_primary_color color-mix(in srgb, @window_fg_color 49.5%, @window_bg_color); @define-color ft_insensitive_primary_color color-mix(in srgb, @window_fg_color 30%, @window_bg_color); @define-color ft_secondary_color color-mix(in srgb, @ft_primary_color 50%, @window_bg_color); @define-color ft_unfocused_secondary_color color-mix(in srgb, @ft_unfocused_primary_color 50%, @window_bg_color); @define-color ft_insensitive_secondary_color color-mix(in srgb, @ft_insensitive_primary_color 50%, @window_bg_color); @define-color ft_support_color color-mix(in srgb, @ft_primary_color 13.5%, @window_bg_color); @define-color ft_unfocused_support_color color-mix(in srgb, @ft_support_color 50%, @window_bg_color); @define-color ft_insensitive_support_color color-mix(in srgb, @ft_support_color 50%, @window_bg_color); /* * Misc */ .ft-header-bar viewswitcher.wide image { opacity: 0.001; /* setting opacity to 0 causes errors */ margin-right: -16px; } .ft-header-bar viewswitcher.wide label { margin-right: 2px; } .keybinding { font-size: 16pt; } .keybinding .key { font-weight: bold; background-color: @theme_base_color; padding: 8px 16px; border: 1px solid alpha(@borders, 0.4); border-radius: 5px; } row.ft-log-scale { padding: 14px 14px 7px 14px; } .ft-list > row.ft-slim-row { padding-top: 8px; padding-bottom: 8px; } .ft-plugin-name { font-weight: bold; } .ft-plugin-description { font-size: small; } .ft-state-button { padding-top: 5px; padding-bottom: 6px; font-size: 1.2em; font-weight: bold; } /** * FtTimerControlButtons widget */ timercontrolbuttons.circular button { border-radius: 50%; } /** * FtSessionProgressBar widget */ sessionprogressbar .through { color: @ft_support_color; } sessionprogressbar:backdrop .through { color: @ft_unfocused_support_color; } sessionprogressbar:disabled .through { color: @ft_insensitive_support_color; } sessionprogressbar .highlight { color: @ft_primary_color; } sessionprogressbar:backdrop .highlight { color: @ft_unfocused_primary_color; } /** * FtTimerProgressBar widget */ timerprogressbar .through { color: @ft_support_color; } timerprogressbar:backdrop .through { color: @ft_unfocused_support_color; } timerprogressbar:disabled .through { color: @ft_insensitive_support_color; } timerprogressbar .highlight { color: @ft_primary_color; } timerprogressbar:backdrop .highlight { color: @ft_unfocused_primary_color; } /** * FtTimerLabel widget */ timerlabel .placeholder { color: @unfocused_borders; } /** * FtTimerView widget */ timerview button:backdrop, timerview button:disabled { color: @insensitive_fg_color; } timerview timerlabel { font-size: 55pt; /* reference size */ font-weight: 400; } timerview sessionprogressbar { padding: 12px 0; margin: 0 3px; } timerview timercontrolbuttons button { padding: 16px; -gtk-icon-size: 24px; } timerview timercontrolbuttons button.suggested-action { padding: 20px; } timerview timercontrolbuttons .suggested-action, timerview timercontrolbuttons .suggested-action:focus, timerview timercontrolbuttons .suggested-action:hover, timerview timercontrolbuttons .suggested-action:active { border: 0; color: @theme_bg_color; background-color: @theme_fg_color; background-image: none; } timerview .suggested-action:focus { outline-width: 3px; outline-offset: 0px; } timerview .suggested-action:backdrop, timerview .suggested-action:backdrop:hover, timerview .suggested-action:disabled { color: @theme_bg_color; background-color: @insensitive_fg_color; } timerview .ft-state-menu > button { padding: 8px 12px 8px 16px; } timerview .ft-state-menu arrow { opacity: 0; margin: 0; margin-left: -16px; } timerview timerlabel:backdrop, timerview timerstatemenu label:backdrop { color: @ft_unfocused_primary_color; } timerview:dir(rtl) .ft-state-menu > button { padding: 8px 16px 8px 12px; } timerview:dir(rtl) .ft-state-menu arrow { margin: 0; margin-right: -16px; } /** * FtCompactTimerView widget */ compacttimerview { min-width: 300px; padding: 5px; } compacttimerview > grid > separator { margin-left: 7px; } compacttimerview .ft-state-menu > button { padding: 3px 5px 3px 9px; } compacttimerview .ft-state-menu arrow { opacity: 0; margin: 0; margin-left: -16px; } compacttimerview timerlabel { font-size: 16pt; font-weight: 600; } compacttimerview timerprogressbar { padding: 0 0 0 9px; } compacttimerview timercontrolbuttons button { padding: 3px; } compacttimerview:dir(rtl) > grid > separator { margin: 0; margin-right: 7px; } compacttimerview:dir(rtl) .ft-state-menu > button { padding: 3px 9px 3px 5px; } compacttimerview:dir(rtl) .ft-state-menu arrow { margin: 0; margin-right: -16px; } compacttimerview:dir(rtl) timerprogressbar { padding: 0 9px 0 0; } compacttimerview:backdrop timerprogressbar .highlight { color: @ft_primary_color; } /** * FtStatsView */ statsview #title > button { padding: 4px 12px 4px 12px; } statsview .title-1 { font-size: 1.6em; font-weight: bold; } statsview .title-2 { font-size: 1.2em; font-weight: bold; } statsview .osd.toolbar { padding: 2px; border-radius: 14px; } statsview .osd.toolbar > button { border: none; padding: 2px; border-radius: 12px; min-height: 20px; min-width: 20px; } statsview chart label, statsview chart chartaxis, statsview statscard label.heading { color: alpha(@theme_fg_color, 0.7); } statsview:backdrop #navigation image, statsview:backdrop label.heading, statsview:backdrop chart label, statsview:backdrop chart chartaxis { color: @insensitive_fg_color; } statsview:backdrop canvas { opacity: 0.95; } /** * ChartGrid widget */ chartgrid { color: @unfocused_borders; } /** * FtStatsCard */ statscard { padding: 12px; } statscard label.heading { font-size: 1em; font-weight: 800; } statscard label.value { font-size: 1.6em; font-weight: 400; } statscard .pill { padding: 1pt 6pt 2pt 6pt; font-size: 10pt; border: none; border-radius: 10pt; background-clip: padding-box; color: #666; background-color: rgba(0, 0, 0, 0.08); } statscard .pill.positive { color: #449e70; background-color: rgba(78, 160, 114, 0.15); } statscard .pill.negative { color: #d24444; background-color: rgba(184, 52, 52, 0.15); } /** * FtBarChart, FtBubbleChart widgets */ chart scrolledwindow undershoot.left { background: linear-gradient(to right, @theme_bg_color, transparent 100%); } chart scrolledwindow undershoot.right { background: linear-gradient(to left, @theme_bg_color, transparent 100%); } chart .bar:not(:disabled):hover, chart .bubble:not(:disabled):hover { filter: contrast(0.75) brightness(1.15); } /** * FtDayChooser widgets */ calendar { border: none; padding: 0; margin: 0; font-size: inherit; } calendar header { border: none; padding: 0; margin: 0; font-size: inherit; font-weight: 600; } calendar grid, calendar box.weekday { font-size: 90%; } calendar box.weekday { margin: 8px 12px 3px 12px; } calendar box.weekday.dim-label { color: alpha(@theme_fg_color, 0.5); opacity: 1; filter: none; } calendar grid button { font-weight: 400; } calendar grid button:selected { color: @accent_fg_color; background-color: @accent_bg_color; font-weight: 600; } calendar grid button:disabled, calendar grid button.dim-label { color: alpha(@theme_fg_color, 0.5); opacity: 1; filter: none; } calendar grid button.dim-label:selected { color: @accent_fg_color; background-color: alpha(@accent_bg_color, 0.5); opacity: 1; filter: none; } calendar grid .week-number { font-weight: 600; } calendar grid .week:selected .day-number { color: alpha(@accent_fg_color, 0.8); } calendar grid .week:selected .day-number.dim-label { color: alpha(@accent_fg_color, 0.6); } calendar grid .circular { padding: 0px; min-height: 32px; } calendar grid .pill { padding: 6px; } calendar grid .pill.week { padding: 0px 12px; font-weight: 400; } /** * FtCondition widget */ condition.card { padding: 6px; } /** * FtConditionGroup widget */ button.small.pill { font-size: 90%; padding: 6px 12px; } menubutton.small.pill { padding: 0; } menubutton.small.pill button { font-size: 90%; padding: 6px 12px; } /* * Screen Overlay */ lightbox { background-color: rgba(0, 0, 0, 1.0); color: white; padding: 0; } lightbox .contents { padding: 20px; } lightbox timerlabel { font-size: 65pt; font-weight: 600; } lightbox .message { margin-top: 10px; font-size: 14pt; } lightbox button { padding: 10px; } lightbox button:hover { background-color: rgba(255, 255, 255, 0.25); } lightbox button:active { background-color: rgba(255, 255, 255, 0.30); } /* * Preferences window */ .preferences .sidebar { border: none; } popover.menu .title.no-padding { padding-left: 10px; } /* * Action edit window */ .events-list list.boxed-list > row:nth-last-child(2) { border-bottom: none; /* hide separator between last row and hidden placeholder */ } /* * Log window */ .navigation-sidebar header { padding-left: 10px; } /* * Shortcut edit window */ shortcut { font-size: 150%; } shortcut label.keycap { padding: 0.4em 0.7em; margin: 0.3em; } /* * FtExtensionDialog widget */ .extension-dialog carouselindicatordots { margin: 0px; } .extension-dialog carousel > box { padding: 18px 24px 24px 24px; } .extension-dialog .icon { color: alpha(currentColor, 0.9); margin-bottom: 24px; } .extension-dialog .title-1 { font-size: 160%; margin-bottom: 20px; } .extension-dialog .title-2 { font-size: 130%; margin-top: 6px; margin-bottom: 16px; } .extension-dialog .description { margin-bottom: 20px; } .extension-dialog .response-area { margin: 0 24px 24px 24px; } .extension-dialog .response-area button { padding: 10px 20px; border-radius: 12px; } .extension-dialog .linked *:first-child { border-radius: 9px 0 0 9px; } .extension-dialog .linked *:last-child { border-radius: 0 9px 9px 0; } .extension-dialog .body { padding: 24px; } .extension-dialog .body .title { padding: 0; margin-bottom: 12px; } .extension-dialog .body .error-message { margin-bottom: 12px; } .extension-dialog textview.monospace { padding: 12px; font-size: inherit; } /* * FtCheckmark widget */ checkmark { background: @ft_primary_color; color: @window_bg_color; border-radius: 50%; padding: 16px; outline: none; } checkmark:backdrop { background: @ft_unfocused_primary_color; } /* * About dialog */ dialog.about .icon-dropshadow { -gtk-icon-shadow: 0px 1px rgba(0, 0, 0, 0.02), 0px -1px rgba(0, 0, 0, 0.02), 1px 0px rgba(0, 0, 0, 0.02), -1px 0px rgba(0, 0, 0, 0.02); } /* * Common */ .no-padding { padding: 0; } .numeric { font-feature-settings: "tnum"; } .small-text { font-size: 80%; font-weight: 400; } .tooltip-contents { font-size: 90%; } .tooltip-header { font-weight: bold; } focustimerhq-FocusTimer-8581be2/src/ui/utils.vala000066400000000000000000000137441520625676500221010ustar00rootroot00000000000000/* * Copyright (c) 2023-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { internal Gdk.RGBA blend_colors (Gdk.RGBA background_color, Gdk.RGBA foreground_color) { var alpha = foreground_color.alpha; return Gdk.RGBA () { red = (1.0f - alpha) * background_color.red + alpha * foreground_color.red, green = (1.0f - alpha) * background_color.green + alpha * foreground_color.green, blue = (1.0f - alpha) * background_color.blue + alpha * foreground_color.blue, alpha = background_color.alpha + (1.0f - background_color.alpha) * alpha, }; } /** * Calculate relative luminance of a color according to WCAG 2.0 * https://www.w3.org/TR/WCAG20/#relativeluminancedef */ internal float get_color_luminance (Gdk.RGBA color) { var r = color.red <= 0.03928f ? color.red / 12.92f : (float) Math.pow ((color.red + 0.055f) / 1.055f, 2.4); var g = color.green <= 0.03928f ? color.green / 12.92f : (float) Math.pow ((color.green + 0.055f) / 1.055f, 2.4); var b = color.blue <= 0.03928f ? color.blue / 12.92f : (float) Math.pow ((color.blue + 0.055f) / 1.055f, 2.4); return 0.2126f * r + 0.7152f * g + 0.0722f * b; } /** * Deduce background color from foreground color based on luminance. * * If foreground is dark (luminance < 0.5), assume light background (98% luminance). * If foreground is bright, assume darker background (20% luminance). */ internal Gdk.RGBA get_background_color (Gdk.RGBA foreground_color) { var foreground_luminance = get_color_luminance (foreground_color); var background_luminance = foreground_luminance < 0.5f ? 0.98f : 0.20f; return Gdk.RGBA () { red = background_luminance, green = background_luminance, blue = background_luminance, alpha = 1.0f }; } /** * Compute primary color for charts from a foreground color. * The primary color is basically a foreground color with no alpha. */ internal Gdk.RGBA get_chart_primary_color (Gdk.RGBA foreground_color) { var background_color = get_background_color (foreground_color); return blend_colors (background_color, foreground_color); } /** * Compute secondary color for charts from a foreground color. */ internal Gdk.RGBA get_chart_secondary_color (Gdk.RGBA foreground_color) { var secondary_color = get_chart_primary_color (foreground_color); secondary_color.alpha *= 0.2f; return secondary_color; } internal unowned Gtk.Widget? get_child_by_buildable_id (Gtk.Widget widget, string buildable_id) { unowned var child = widget.get_first_child (); while (child != null) { if (child.get_buildable_id () == buildable_id) { return child; } unowned var nested_child = get_child_by_buildable_id (child, buildable_id); if (nested_child != null) { return nested_child; } child = child.get_next_sibling (); } return null; } internal string capitalize_words (string text) { var string_builder = new GLib.StringBuilder (); var capitalize_next = true; int index = 0; unichar chr; while (text.get_next_char (ref index, out chr)) { if (chr == ' ') { string_builder.append_unichar (chr); capitalize_next = true; } else if (capitalize_next && chr.islower ()) { string_builder.append_unichar (chr.toupper ()); capitalize_next = false; } else { string_builder.append_unichar (chr); capitalize_next = false; } } return string_builder.str; } /** * Function to get xdg-desktop-portal compatible window ID * * https://flatpak.github.io/xdg-desktop-portal/docs/window-identifiers.html */ internal async string get_window_identifier (Gtk.Window? window) { var surface = window?.get_surface (); var display = surface?.get_display (); #if HAVE_GDK_WAYLAND if (display is Gdk.Wayland.Display) { var wayland_toplevel = surface as Gdk.Wayland.Toplevel; string? handle = null; if (wayland_toplevel != null) { var wait_for_handle = wayland_toplevel.export_handle ( (_toplevel, _handle) => { handle = _handle; get_window_identifier.callback (); }); if (wait_for_handle) { yield; } return handle != null ? @"wayland:$(handle)" : ""; } } #endif // TODO: test this #if HAVE_GDK_X11 if (display is Gdk.X11.Display) { var x11_surface = surface as Gdk.X11.Surface; if (x11_surface != null) { var xid = (int) x11_surface.get_xid (); return @"x11:$(xid)"; } } #endif return ""; } internal inline Gtk.Orientation get_opposite_orientation (Gtk.Orientation orientation) { return orientation == Gtk.Orientation.HORIZONTAL ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; } internal void normalize_rectangle (ref Gdk.Rectangle rect) { if (rect.width < 0) { rect.x += rect.width; rect.width = rect.width.abs (); } if (rect.height < 0) { rect.y += rect.height; rect.height = rect.height.abs (); } } } focustimerhq-FocusTimer-8581be2/src/ui/widgets/000077500000000000000000000000001520625676500215315ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/src/ui/widgets/checkmark.vala000066400000000000000000000157111520625676500243330ustar00rootroot00000000000000/* * Copyright (c) 2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { public class Checkmark : Gtk.Widget { private const uint DEFAULT_PIXEL_SIZE = 32; private const uint DEFAULT_DELAY = 500; private const uint LINE_WIDTH = 6; [CCode (notify = false)] public uint pixel_size { get { return this._pixel_size; } set { if (this._pixel_size != value) { this._pixel_size = value; this.notify_property ("pixel-size"); this.queue_resize (); } } } [CCode (notify = false)] public uint delay { get { return this._delay; } set { if (this._delay != value) { this._delay = value; this.notify_property ("delay"); this.animate (); } } } private uint _delay = DEFAULT_DELAY; private uint _pixel_size = DEFAULT_PIXEL_SIZE; private uint _line_width = LINE_WIDTH; private Adw.TimedAnimation? first_animation = null; private Adw.TimedAnimation? second_animation = null; private uint timeout_id = 0U; private bool animation_done = false; static construct { set_css_name ("checkmark"); } private void on_second_animation_done () { this.first_animation = null; this.second_animation = null; this.animation_done = true; } private void on_first_animation_done () { this.second_animation.play (); } private void animate () { if (this.first_animation != null) { this.first_animation.pause (); this.first_animation = null; } if (this.second_animation != null) { this.second_animation.pause (); this.second_animation = null; } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } var animation_target = new Adw.CallbackAnimationTarget (this.queue_draw); this.first_animation = new Adw.TimedAnimation (this, 0.0, 1.0, 200, animation_target); this.first_animation.set_easing (Adw.Easing.EASE_IN_OUT_CUBIC); this.first_animation.done.connect (this.on_first_animation_done); this.second_animation = new Adw.TimedAnimation (this, 0.0, 1.0, 300, animation_target); this.second_animation.set_easing (Adw.Easing.EASE_OUT_QUAD); this.second_animation.done.connect (this.on_second_animation_done); this.timeout_id = GLib.Timeout.add ( this._delay, () => { this.timeout_id = 0; if (this.first_animation != null) { this.first_animation.play (); } return GLib.Source.REMOVE; }); GLib.Source.set_name_by_id (this.timeout_id, "Ft.Checkmark.animate"); this.animation_done = false; } public override void map () { base.map (); this.animate (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { minimum = (int) this._pixel_size; natural = (int) this._pixel_size; minimum_baseline = -1; natural_baseline = -1; } public override void snapshot (Gtk.Snapshot snapshot) { var width = (float) this.get_width (); var height = (float) this.get_height (); var origin = Graphene.Point () { x = width / 2.0f, y = height / 2.0f }; var scale = (float.min (width, height) - this._line_width) / 2.0f; var color = this.get_color (); var x_0 = origin.x - 1.000f * scale; var y_0 = origin.y - 0.025f * scale; var x_1 = origin.x - 0.250f * scale; var y_1 = origin.y + 0.725f * scale; var x_2 = origin.x + 1.000f * scale; var y_2 = origin.y - 0.525f * scale; var progress_1 = this.first_animation != null ? (float) this.first_animation.value : (this.animation_done ? 1.0f : 0.0f); var progress_2 = this.second_animation != null ? (float) this.second_animation.value : (this.animation_done ? 1.0f : 0.0f); var path_builder = new Gsk.PathBuilder (); path_builder.move_to (x_0, y_0); path_builder.rel_line_to ((x_1 - x_0) * progress_1, (y_1 - y_0) * progress_1); path_builder.rel_line_to ((x_2 - x_1) * progress_2, (y_2 - y_1) * progress_2); var stroke = new Gsk.Stroke (this._line_width); stroke.set_line_cap (Gsk.LineCap.ROUND); stroke.set_line_join (Gsk.LineJoin.ROUND); snapshot.append_stroke (path_builder.to_path (), stroke, color); } public override void dispose () { if (this.first_animation != null) { this.first_animation.pause (); this.first_animation = null; } if (this.second_animation != null) { this.second_animation.pause (); this.second_animation = null; } if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/widgets/gizmo.vala000066400000000000000000000136711520625676500235330ustar00rootroot00000000000000/* * Copyright (c) 2022-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { public delegate void GizmoMeasureFunc (Ft.Gizmo gizmo, Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline); public delegate void GizmoAllocateFunc (Ft.Gizmo gizmo, int width, int height, int baseline); public delegate void GizmoSnapshotFunc (Ft.Gizmo gizmo, Gtk.Snapshot snapshot); public delegate bool GizmoContainsFunc (Ft.Gizmo gizmo, double x, double y); public delegate bool GizmoFocusFunc (Ft.Gizmo gizmo, Gtk.DirectionType direction); public delegate bool GizmoGrabFocusFunc (Ft.Gizmo gizmo); /** * A widget that is controlled by its parent. * * It's a carbon copy from Gtk+ code. File gtk/gtk/gtkgizmo.c */ public sealed class Gizmo : Gtk.Widget { private Ft.GizmoMeasureFunc? measure_func; private Ft.GizmoAllocateFunc? allocate_func; private Ft.GizmoSnapshotFunc? snapshot_func; private Ft.GizmoContainsFunc? contains_func; private Ft.GizmoFocusFunc? focus_func; private Ft.GizmoGrabFocusFunc? grab_focus_func; public Gizmo (owned Ft.GizmoMeasureFunc? measure_func, owned Ft.GizmoAllocateFunc? allocate_func, owned Ft.GizmoSnapshotFunc? snapshot_func, owned Ft.GizmoContainsFunc? contains_func, owned Ft.GizmoFocusFunc? focus_func, owned Ft.GizmoGrabFocusFunc? grab_focus_func) { this.measure_func = (owned) measure_func; this.allocate_func = (owned) allocate_func; this.snapshot_func = (owned) snapshot_func; this.contains_func = (owned) contains_func; this.focus_func = (owned) focus_func; this.grab_focus_func = (owned) grab_focus_func; } public Gizmo.with_role (Gtk.AccessibleRole role, owned Ft.GizmoMeasureFunc? measure_func, owned Ft.GizmoAllocateFunc? allocate_func, owned Ft.GizmoSnapshotFunc? snapshot_func, owned Ft.GizmoContainsFunc? contains_func, owned Ft.GizmoFocusFunc? focus_func, owned Ft.GizmoGrabFocusFunc? grab_focus_func) { GLib.Object ( accessible_role: role ); this.measure_func = (owned) measure_func; this.allocate_func = (owned) allocate_func; this.snapshot_func = (owned) snapshot_func; this.contains_func = (owned) contains_func; this.focus_func = (owned) focus_func; this.grab_focus_func = (owned) grab_focus_func; } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { if (this.measure_func != null) { this.measure_func (this, orientation, for_size, out minimum, out natural, out minimum_baseline, out natural_baseline); } else { minimum = 0; natural = for_size; minimum_baseline = -1; natural_baseline = -1; } } public override void size_allocate (int width, int height, int baseline) { if (this.allocate_func != null) { this.allocate_func (this, width, height, baseline); } } public override void snapshot (Gtk.Snapshot snapshot) { if (this.snapshot_func != null) { this.snapshot_func (this, snapshot); } else { base.snapshot (snapshot); } } public override bool contains (double x, double y) { if (this.contains_func != null) { return this.contains_func (this, x, y); } return base.contains (x, y); } public override bool focus (Gtk.DirectionType direction) { if (this.focus_func != null) { return this.focus_func (this, direction); } return false; } public override bool grab_focus () { if (this.grab_focus_func != null) { return this.grab_focus_func (this); } return false; } public override void dispose () { this.measure_func = null; this.allocate_func = null; this.snapshot_func = null; this.contains_func = null; this.focus_func = null; this.grab_focus_func = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/src/ui/widgets/monospace-label.vala000066400000000000000000000165411520625676500254460ustar00rootroot00000000000000/* * Copyright (c) 2021-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Ft { // TODO: rename to NumericLabel /** * Optimized version of Gtk.Label that doesn't trigger size allocation on text changes. * It also simplifies scaling and uses monospace width for digits. */ public class MonospaceLabel : Gtk.Widget { public string text { get { return this._text; } set { if (this._text == value) { return; } var previous_text_length = this._text != null ? this._text.length : 0; this._text = value; if (this.layout != null && previous_text_length == value.length) { this.layout.set_text (value, value.length); this.queue_draw (); } else { this.clear_layout (); this.queue_resize (); } } } public float xalign { get { return this._xalign; } set { this._xalign = value; this.queue_allocate (); } } public float yalign { get { return this._yalign; } set { this._yalign = value; this.queue_allocate (); } } public double scale { get { return this._scale; } set { if (this._scale == value) { return; } this._scale = value; this.clear_layout (); this.queue_resize (); } } private string _text = null; private float _xalign = 0.5f; private float _yalign = 0.5f; private double _scale = 1.0; private Pango.Layout? layout = null; private int layout_x = 0; private int layout_y = 0; private int layout_width = 0; private int layout_height = 0; private int layout_baseline = -1; static construct { set_css_name ("label"); } construct { this.accessible_role = Gtk.AccessibleRole.LABEL; this._text = ""; this.layout = null; } internal Pango.Layout create_pango_layout_with_scale (string text, double scale) { var context = this.create_pango_context (); var attributes = new Pango.AttrList (); attributes.insert (Pango.attr_scale_new (scale)); attributes.insert (new Pango.AttrFontFeatures ("tnum")); var layout = new Pango.Layout (context); layout.set_ellipsize (Pango.EllipsizeMode.NONE); layout.set_attributes (attributes); layout.set_text (text, text.length); return layout; } private void measure_pango_layout (Pango.Layout layout, out int width, out int height, out int baseline) { var text = layout.get_text (); var is_numeric = int.try_parse (text); baseline = layout.get_baseline () / Pango.SCALE; if (is_numeric) { var reference_text = string.nfill (text.length, '0'); var reference_layout = layout.copy (); reference_layout.set_text (reference_text, reference_text.length); reference_layout.get_pixel_size (out width, out height); } else { layout.get_pixel_size (out width, out height); } } private void ensure_layout () { if (this.layout == null) { this.layout = this.create_pango_layout_with_scale (this._text, this._scale); this.measure_pango_layout (this.layout, out this.layout_width, out this.layout_height, out this.layout_baseline); } } private void clear_layout () { this.layout = null; this.layout_x = 0; this.layout_y = 0; this.layout_width = 0; this.layout_height = 0; this.layout_baseline = -1; } public override void css_changed (Gtk.CssStyleChange change) { base.css_changed (change); this.clear_layout (); this.queue_resize (); } public override Gtk.SizeRequestMode get_request_mode () { return Gtk.SizeRequestMode.CONSTANT_SIZE; } public override void measure (Gtk.Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { this.ensure_layout (); if (orientation == Gtk.Orientation.HORIZONTAL) { minimum = this.layout_width; natural = minimum; minimum_baseline = -1; natural_baseline = -1; } else { minimum = this.layout_height; natural = minimum; minimum_baseline = this.layout_baseline; natural_baseline = this.layout_baseline; } } public override void size_allocate (int width, int height, int baseline) { this.ensure_layout (); this.layout_x = (int) Math.floorf ((float)(width - this.layout_width) * this._xalign); this.layout_y = baseline != -1 ? baseline - this.layout_baseline : (int) Math.floorf ((float)(height - this.layout_height) * this._yalign); if (this.get_direction () == Gtk.TextDirection.RTL) { this.layout_x = width - this.layout_x; } } public override void snapshot (Gtk.Snapshot snapshot) requires (this.layout != null) { var origin = Graphene.Point () { x = (float) this.layout_x, y = (float) this.layout_y }; snapshot.translate (origin); snapshot.append_layout (this.layout, this.get_color ()); } public override bool focus (Gtk.DirectionType direction) { return false; } public override bool grab_focus () { return false; } public override void unroot () { base.unroot (); this.clear_layout (); } } } focustimerhq-FocusTimer-8581be2/src/ui/widgets/sidebar-row.ui000066400000000000000000000014521520625676500243100ustar00rootroot00000000000000 focustimerhq-FocusTimer-8581be2/src/ui/widgets/sidebar-row.vala000066400000000000000000000024201520625676500246120ustar00rootroot00000000000000/* * Copyright (c) 2024-2025 focus-timer contributors * * SPDX-License-Identifier: GPL-3.0-or-later */ namespace Ft { [GtkTemplate (ui = "/io/github/focustimerhq/FocusTimer/ui/widgets/sidebar-row.ui")] public class SidebarRow : Gtk.Box { public string icon_name { get { return this._icon_name; } set { this._icon_name = value; this.icon.visible = value != ""; } } public string title { get; set; } public Gtk.Widget? suffix { get { return this._suffix; } set { if (this._suffix != null) { base.remove (this._suffix); } this._suffix = value; if (this._suffix != null) { base.append (this._suffix); } } } [GtkChild] private unowned Gtk.Image icon; private string _icon_name; private Gtk.Widget? _suffix = null; static construct { set_css_name ("row"); } public override void dispose () { this._suffix = null; base.dispose (); } } } focustimerhq-FocusTimer-8581be2/tests/000077500000000000000000000000001520625676500200215ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/tests/common.vala000066400000000000000000000151601520625676500221610ustar00rootroot00000000000000/* * Copyright (c) 2014 focus-timer contributors * * This code is partly borrowed from libgee's test suite, * at https://git.gnome.org/browse/libgee and from gnome-break-timer * https://git.gnome.org/browse/gnome-break-timer * * 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, either version 3 of the License, or * (at your option) any later version. * * 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. If not, see . * */ using GLib; namespace Tests { public delegate void TestCaseFunc (); private string str_to_representation (string value) { return @"\"$value\""; } private string strv_to_representation (string[] value) { var string_builder = new GLib.StringBuilder ("["); var length = value.length; for (var index = 0; index < length; index++) { if (index > 0) { string_builder.append (", "); } string_builder.append (str_to_representation (value[index])); } string_builder.append ("]"); return string_builder.str; } public void assert_cmpstrv (string[] value, string[] expected) { if (GLib.Test.failed ()) { return; } var length = value.length; if (length != expected.length) { GLib.Test.message ( "Arrays have different length: %s != %s", strv_to_representation (value), strv_to_representation (expected) ); GLib.Test.fail (); return; } for (var index = 0; index < length; index++) { if (value[index] != expected[index]) { GLib.Test.message ( "Arrays are not equal: %s != %s", strv_to_representation (value), strv_to_representation (expected) ); GLib.Test.fail (); return; } } } public void wait_for_object_finalized (owned GLib.Object object) { void* weak_object = object; object.add_weak_pointer (&weak_object); object = null; assert_null (weak_object); } private class TestCase { public string name; private Tests.TestCaseFunc func; private Tests.TestSuite test_suite; public TestCase (string name, owned Tests.TestCaseFunc test_case_func, Tests.TestSuite test_suite) { this.name = name; this.func = (owned) test_case_func; this.test_suite = test_suite; } public void setup (void* fixture) { this.test_suite.setup (); } public void run (void* fixture) { this.func (); } public void teardown (void* fixture) { this.test_suite.teardown (); } public GLib.TestCase get_g_test_case () { return new GLib.TestCase (this.name, this.setup, this.run, this.teardown); } } public abstract class TestSuite : GLib.Object { private GLib.TestSuite g_test_suite; private Tests.TestCase[] test_cases; construct { this.g_test_suite = new GLib.TestSuite (this.get_name ()); this.test_cases = new Tests.TestCase[0]; } public string get_name () { return this.get_type ().name (); } public GLib.TestSuite get_g_test_suite () { return this.g_test_suite; } public void add_test (string name, owned Tests.TestCaseFunc func) { var test_case = new TestCase (name, (owned) func, this); this.test_cases += test_case; this.g_test_suite.add (test_case.get_g_test_case ()); } public virtual void setup () { } public virtual void teardown () { } } public abstract class MainLoopTestSuite : TestSuite { private uint timeout_id = 0; protected GLib.MainLoop? main_loop; protected bool run_main_loop (uint timeout = 1000) requires (this.timeout_id == 0) { var success = true; this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } protected void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } public override void setup () { base.setup (); this.main_loop = new GLib.MainLoop (); } public override void teardown () { base.teardown (); this.main_loop = null; } } public static void init (string[] args) { GLib.Test.init (ref args); // Undo changes made by Test.init(), don't make warnings fatal. GLib.Log.set_always_fatal (GLib.LogLevelFlags.LEVEL_ERROR | GLib.LogLevelFlags.LEVEL_CRITICAL); } public static int run (Tests.TestSuite test_suite, ...) { var arguments_list = va_list (); var root_suite = GLib.TestSuite.get_root (); root_suite.add_suite (test_suite.get_g_test_suite ()); while (true) { Tests.TestSuite? extra_test_suite = arguments_list.arg (); if (extra_test_suite != null) { root_suite.add_suite (extra_test_suite.get_g_test_suite ()); } else { // End of the list. break; } } return GLib.Test.run (); } } focustimerhq-FocusTimer-8581be2/tests/meson.build000066400000000000000000000026041520625676500221650ustar00rootroot00000000000000# Define a common library for all tests. libtests_common = static_library( 'tests', 'common.vala', dependencies: libft_core_dep, include_directories: config_h_dir, install: false ) # Define tests. Each test is a separate executable. test_names = [ 'event-bus', 'event-producer', 'job-queue', 'expression', 'expression-parser', 'provider-set', 'provided-object', 'cycle', 'time-block', 'timer', 'timestamp', 'timezone-history', 'scheduler', 'session', 'session-manager', 'stats-manager', 'database', 'notification-manager', 'command', 'action-manager', 'matrix', 'utils', ] test_env = environment() test_env.set('LANGUAGE', '') test_env.set('LANG', 'C') test_env.set('G_TEST_SRCDIR', meson.current_source_dir()) test_env.set('G_TEST_BUILDDIR', meson.current_build_dir()) test_env.set('G_DEBUG', 'gc-friendly') test_env.set('GSETTINGS_SCHEMA_DIR', meson.project_build_root() / 'data') test_env.set('GSETTINGS_BACKEND', 'memory') test_env.set('MALLOC_CHECK_', '2') foreach test_name : test_names test_sources = [ f'test-@test_name@.vala', compiled_schemas, resources, ] test( test_name, executable( f'test-@test_name@', test_sources, dependencies: libft_core_dep, link_with: libtests_common, c_args: [ '-UG_DISABLE_ASSERT', ], ), env: test_env, suite: 'tests', ) endforeachfocustimerhq-FocusTimer-8581be2/tests/test-action-manager.vala000066400000000000000000000577161520625676500245500ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class EventActionTest : Tests.TestSuite { private string uuid; public EventActionTest () { this.add_test ("load", this.test_load); this.add_test ("save", this.test_save); } public override void setup () { this.uuid = GLib.Uuid.string_random (); } public void test_load () { var settings = new GLib.Settings.with_path ( "io.github.focustimerhq.FocusTimer.actions.action", @"/io/github/focustimerhq/FocusTimer/actions/$(this.uuid)/"); settings.set_enum ("trigger", Ft.ActionTrigger.EVENT); settings.set_boolean ("enabled", true); settings.set_string ("display-name", "Event Action"); settings.set_strv ("events", {"start", "stop"}); settings.set_string ("condition", "isRunning"); settings.set_boolean ("wait-for-completion", true); settings.set_string ("command", "echo Event"); settings.set_string ("working-directory", "/tmp"); settings.set_boolean ("use-subshell", true); settings.set_boolean ("pass-input", true); var action = new Ft.EventAction (this.uuid); action.load (settings); assert_true (action.enabled); assert_cmpstr (action.display_name, GLib.CompareOperator.EQ, "Event Action"); assert_cmpstrv (action.event_names, {"start", "stop"}); assert_nonnull (action.condition); assert_cmpstr (action.condition.to_string (), GLib.CompareOperator.EQ, "isRunning"); assert_true (action.wait_for_completion); assert_cmpstr (action.command.line, GLib.CompareOperator.EQ, "echo Event"); assert_cmpstr (action.command.working_directory, GLib.CompareOperator.EQ, "/tmp"); assert_true (action.command.use_subshell); assert_true (action.command.pass_input); } public void test_save () { var settings = new GLib.Settings.with_path ( "io.github.focustimerhq.FocusTimer.actions.action", @"/io/github/focustimerhq/FocusTimer/actions/$(uuid)/"); var action = new Ft.EventAction (this.uuid); action.display_name = "Event Action"; action.event_names = {"start", "stop"}; action.condition = new Ft.Variable ("is-running"); action.wait_for_completion = true; action.command = new Ft.Command ("echo Event"); action.command.working_directory = "/tmp"; action.command.use_subshell = true; action.command.pass_input = true; action.save (settings); assert_true (settings.get_boolean ("enabled")); assert_cmpstr (settings.get_string ("display-name"), GLib.CompareOperator.EQ, "Event Action"); assert_cmpstrv (settings.get_strv ("events"), {"start", "stop"}); assert_cmpstr (settings.get_string ("condition"), GLib.CompareOperator.EQ, "isRunning"); assert_true (settings.get_boolean ("wait-for-completion")); assert_cmpstr (settings.get_string ("command"), GLib.CompareOperator.EQ, "echo Event"); assert_cmpstr (settings.get_string ("working-directory"), GLib.CompareOperator.EQ, "/tmp"); assert_true (settings.get_boolean ("use-subshell")); assert_true (settings.get_boolean ("pass-input")); } } public class ConditionActionTest : Tests.TestSuite { private string uuid; public ConditionActionTest () { this.add_test ("load", this.test_load); this.add_test ("save", this.test_save); } public override void setup () { this.uuid = GLib.Uuid.string_random (); } public void test_load () { var settings = new GLib.Settings.with_path ( "io.github.focustimerhq.FocusTimer.actions.action", @"/io/github/focustimerhq/FocusTimer/actions/$(this.uuid)/"); settings.set_enum ("trigger", Ft.ActionTrigger.CONDITION); settings.set_boolean ("enabled", true); settings.set_string ("display-name", "Condition Action"); settings.set_string ("condition", "isRunning"); settings.set_string ("command", "echo Enter"); settings.set_string ("exit-command", "echo Exit"); settings.set_string ("working-directory", "/tmp"); settings.set_boolean ("use-subshell", true); settings.set_boolean ("pass-input", true); var action = new Ft.ConditionAction (this.uuid); action.load (settings); assert_true (action.enabled); assert_cmpstr (action.display_name, GLib.CompareOperator.EQ, "Condition Action"); assert_nonnull (action.condition); assert_cmpstr (action.condition.to_string (), GLib.CompareOperator.EQ, "isRunning"); assert_cmpstr (action.enter_command.line, GLib.CompareOperator.EQ, "echo Enter"); assert_cmpstr (action.enter_command.working_directory, GLib.CompareOperator.EQ, "/tmp"); assert_true (action.enter_command.use_subshell); assert_true (action.enter_command.pass_input); assert_cmpstr (action.exit_command.line, GLib.CompareOperator.EQ, "echo Exit"); assert_cmpstr (action.exit_command.working_directory, GLib.CompareOperator.EQ, "/tmp"); assert_true (action.exit_command.use_subshell); assert_true (action.exit_command.pass_input); } public void test_save () { var settings = new GLib.Settings.with_path ( "io.github.focustimerhq.FocusTimer.actions.action", @"/io/github/focustimerhq/FocusTimer/actions/$(this.uuid)/"); var action = new Ft.ConditionAction (this.uuid); action.display_name = "Condition Action"; action.condition = new Ft.Variable ("is-running"); action.enter_command = new Ft.Command ("echo Enter"); action.exit_command = new Ft.Command ("echo Exit"); action.exit_command.working_directory = action.enter_command.working_directory = "/tmp"; action.exit_command.use_subshell = action.enter_command.use_subshell = true; action.exit_command.pass_input = action.enter_command.pass_input = true; action.save (settings); assert_cmpuint (settings.get_enum ("trigger"), GLib.CompareOperator.EQ, Ft.ActionTrigger.CONDITION); assert_true (settings.get_boolean ("enabled")); assert_cmpstr (settings.get_string ("display-name"), GLib.CompareOperator.EQ, "Condition Action"); assert_cmpstr (settings.get_string ("condition"), GLib.CompareOperator.EQ, "isRunning"); assert_cmpstr (settings.get_string ("command"), GLib.CompareOperator.EQ, "echo Enter"); assert_cmpstr (settings.get_string ("exit-command"), GLib.CompareOperator.EQ, "echo Exit"); assert_cmpstr (settings.get_string ("working-directory"), GLib.CompareOperator.EQ, "/tmp"); assert_true (settings.get_boolean ("use-subshell")); assert_true (settings.get_boolean ("pass-input")); } } public class ActionListModelTest : Tests.TestSuite { private GLib.Settings? settings; public ActionListModelTest () { this.add_test ("save_action__create", this.test_save_action__create); this.add_test ("save_action__update", this.test_save_action__update); this.add_test ("delete_action", this.test_delete_action); this.add_test ("move_action", this.test_move_action); } public override void setup () { this.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer.actions"); this.settings.set_strv ("actions", {}); } public override void teardown () { this.settings = null; } public void test_save_action__create () { var model = new Ft.ActionListModel (); assert_true (model.get_item_type () == typeof (Ft.Action)); assert_cmpuint (model.get_n_items (), GLib.CompareOperator.EQ, 0U); assert_cmpuint (model.n_items, GLib.CompareOperator.EQ, 0U); assert_null (model.get_item (0)); var observed_position = 999U; var observed_removed = 999U; var observed_added = 999U; var signals_count = 0U; model.items_changed.connect ((position, removed, added) => { observed_position = position; observed_removed = removed; observed_added = added; signals_count++; }); var action = new Ft.EventAction (null); action.display_name = "Action"; action.command = new Ft.Command ("echo Action"); action.event_names = {"start"}; model.save_action (action); assert_cmpuint (signals_count, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_position, GLib.CompareOperator.EQ, 0U); assert_cmpuint (observed_removed, GLib.CompareOperator.EQ, 0U); assert_cmpuint (observed_added, GLib.CompareOperator.EQ, 1U); assert_cmpuint (model.get_n_items (), GLib.CompareOperator.EQ, 1U); assert_nonnull (model.get_item (0)); assert_null (model.get_item (1)); // index and lookup must be consistent var uuid = action.uuid; assert_true (uuid != null && uuid != ""); assert_cmpint (model.index (uuid), GLib.CompareOperator.EQ, 0); assert (model.lookup (uuid) == action); // After first get_item, subsequent calls should return same instance var item_0 = (Ft.Action) model.get_item (0); var item_0_again = (Ft.Action) model.get_item (0); assert (item_0 == item_0_again); } public void test_save_action__update () { var model = new Ft.ActionListModel (); var action_1 = new Ft.EventAction ("00000000-0000-0000-0000-000000000000"); action_1.display_name = "Action 1"; action_1.command = new Ft.Command ("echo 1"); action_1.event_names = {"start"}; model.save_action (action_1); var observed_position = 999U; var observed_removed = 999U; var observed_added = 999U; var signals_count = 0U; model.items_changed.connect ((position, removed, added) => { observed_position = position; observed_removed = removed; observed_added = added; signals_count++; }); var action_2 = new Ft.EventAction (action_1.uuid); action_2.display_name = "Action 2"; action_2.command = new Ft.Command ("echo 2"); action_2.event_names = {"resume"}; model.save_action (action_2); // Updating should not change length, but should notify 1 removed, 1 added at same position assert_cmpuint (signals_count, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_position, GLib.CompareOperator.EQ, 0U); assert_cmpuint (observed_removed, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_added, GLib.CompareOperator.EQ, 1U); assert_cmpuint (model.get_n_items (), GLib.CompareOperator.EQ, 1U); var action = (Ft.EventAction?) model.lookup (action_1.uuid); assert_cmpstr (action.display_name, GLib.CompareOperator.EQ, action_2.display_name); assert_cmpstr (action.command.line, GLib.CompareOperator.EQ, action_2.command.line); assert_cmpstrv (action.event_names, action_2.event_names); } public void test_delete_action () { var model = new Ft.ActionListModel (); var action = new Ft.EventAction (null); action.display_name = "Action"; action.command = new Ft.Command ("echo Action"); action.event_names = {"start"}; model.save_action (action); var observed_position = 999U; var observed_removed = 999U; var observed_added = 999U; var signals_count = 0U; model.items_changed.connect ((position, removed, added) => { observed_position = position; observed_removed = removed; observed_added = added; signals_count++; }); model.delete_action (action.uuid); assert_cmpuint (signals_count, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_position, GLib.CompareOperator.GE, 0U); assert_cmpuint (observed_removed, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_added, GLib.CompareOperator.EQ, 0U); assert_cmpuint (model.get_n_items (), GLib.CompareOperator.EQ, 0U); assert_null (model.lookup (action.uuid)); } public void test_move_action () { var model = new Ft.ActionListModel (); var action_1 = new Ft.EventAction (null); action_1.display_name = "Action 1"; action_1.command = new Ft.Command ("echo 1"); action_1.event_names = {"start"}; model.save_action (action_1); var action_2 = new Ft.EventAction (null); action_2.display_name = "Action 2"; action_2.command = new Ft.Command ("echo 2"); action_2.event_names = {"start"}; model.save_action (action_2); var action_3 = new Ft.EventAction (null); action_3.display_name = "Action 3"; action_3.command = new Ft.Command ("echo 3"); action_3.event_names = {"start"}; model.save_action (action_3); assert_cmpuint (model.get_n_items (), GLib.CompareOperator.EQ, 3U); assert_cmpint (model.index (action_1.uuid), GLib.CompareOperator.EQ, 0); assert_cmpint (model.index (action_2.uuid), GLib.CompareOperator.EQ, 1); assert_cmpint (model.index (action_3.uuid), GLib.CompareOperator.EQ, 2); var observed_position = 999U; var observed_removed = 999U; var observed_added = 999U; var signals_count = 0U; model.items_changed.connect ((position, removed, added) => { observed_position = position; observed_removed = removed; observed_added = added; signals_count++; }); // Move first to last model.move_action (action_1.uuid, 2U); assert_cmpuint (signals_count, GLib.CompareOperator.EQ, 1U); assert_cmpuint (observed_position, GLib.CompareOperator.EQ, 0U); assert_cmpuint (observed_removed, GLib.CompareOperator.EQ, 2U); assert_cmpuint (observed_added, GLib.CompareOperator.EQ, 2U); assert_cmpint (model.index (action_2.uuid), GLib.CompareOperator.EQ, 0); assert_cmpint (model.index (action_3.uuid), GLib.CompareOperator.EQ, 1); assert_cmpint (model.index (action_1.uuid), GLib.CompareOperator.EQ, 2); // Verify get_item ordering assert ((Ft.Action) model.get_item (0) == model.lookup (action_2.uuid)); assert ((Ft.Action) model.get_item (1) == model.lookup (action_3.uuid)); assert ((Ft.Action) model.get_item (2) == model.lookup (action_1.uuid)); } } public class ActionManagerTest : Tests.TestSuite { private class DummyEventAction : Ft.EventAction { public uint bind_count { get; private set; default = 0U; } public uint unbind_count { get; private set; default = 0U; } public DummyEventAction (string? uuid = null) { base (uuid); } public override void bind () { this.bind_count++; base.bind (); } public override void unbind () { this.unbind_count++; base.unbind (); } } private class DummyConditionAction : Ft.ConditionAction { public uint bind_count { get; private set; default = 0U; } public uint unbind_count { get; private set; default = 0U; } public DummyConditionAction (string? uuid = null) { base (uuid); } public override void bind () { this.bind_count++; base.bind (); } public override void unbind () { this.unbind_count++; base.unbind (); } } private GLib.Settings? settings; public ActionManagerTest () { this.add_test ("save_event_action", this.test_save_event_action); this.add_test ("delete_event_action", this.test_delete_event_action); this.add_test ("save_condition_action", this.test_save_condition_action); this.add_test ("delete_condition_action", this.test_delete_condition_action); this.add_test ("destroy", this.test_destroy); } public override void setup () { this.settings = new GLib.Settings ("io.github.focustimerhq.FocusTimer.actions"); this.settings.set_strv ("actions", {}); } public override void teardown () { this.settings = null; } public void test_save_event_action () { var manager = new Ft.ActionManager (); var action = new DummyEventAction (null); action.display_name = "Action"; action.command = new Ft.Command ("echo Action"); action.event_names = {"start"}; manager.model.save_action (action); // Manager should bind newly added enabled action assert_cmpuint (action.bind_count, GLib.CompareOperator.EQ, 1U); // Toggling enabled should sync to settings and call unbind/bind assert_true (action.settings.get_boolean ("enabled")); action.enabled = false; assert_false (action.settings.get_boolean ("enabled")); assert_cmpuint (action.unbind_count, GLib.CompareOperator.EQ, 1U); action.enabled = true; assert_true (action.settings.get_boolean ("enabled")); assert_cmpuint (action.bind_count, GLib.CompareOperator.EQ, 2U); } public void test_delete_event_action () { var manager = new Ft.ActionManager (); var action = new DummyEventAction (null); action.display_name = "Action"; action.command = new Ft.Command ("echo Action"); action.event_names = {"start"}; manager.model.save_action (action); // Flip to false once to verify sync while connected action.enabled = false; assert_false (action.settings.get_boolean ("enabled")); // Delete should unbind and disconnect property handler manager.model.delete_action (action.uuid); assert_cmpuint (action.unbind_count, GLib.CompareOperator.GE, 1U); // After removal, toggling enabled should not sync to settings anymore var enabled_before = action.settings.get_boolean ("enabled"); action.enabled = !enabled_before; assert_cmpint ((int) action.settings.get_boolean ("enabled"), GLib.CompareOperator.EQ, (int) enabled_before); } public void test_save_condition_action () { var manager = new Ft.ActionManager (); var action = new DummyConditionAction (null); action.display_name = "Condition"; action.condition = new Ft.Variable ("is-running"); action.enter_command = new Ft.Command ("echo Enter"); action.exit_command = new Ft.Command ("echo Exit"); manager.model.save_action (action); // With condition set and enabled, manager should bind the action assert_cmpuint (action.bind_count, GLib.CompareOperator.EQ, 1U); // Toggling enabled should sync and unbind/bind assert_true (action.settings.get_boolean ("enabled")); action.enabled = false; assert_false (action.settings.get_boolean ("enabled")); assert_cmpuint (action.unbind_count, GLib.CompareOperator.EQ, 1U); action.enabled = true; assert_true (action.settings.get_boolean ("enabled")); assert_cmpuint (action.bind_count, GLib.CompareOperator.EQ, 2U); } public void test_delete_condition_action () { var manager = new Ft.ActionManager (); var action = new DummyConditionAction (null); action.display_name = "Condition"; action.condition = new Ft.Variable ("is-running"); action.enter_command = new Ft.Command ("echo Enter"); action.exit_command = new Ft.Command ("echo Exit"); manager.model.save_action (action); action.enabled = false; assert_false (action.settings.get_boolean ("enabled")); manager.model.delete_action (action.uuid); assert_cmpuint (action.unbind_count, GLib.CompareOperator.GE, 1U); var enabled_before = action.settings.get_boolean ("enabled"); action.enabled = !enabled_before; assert_cmpint ((int) action.settings.get_boolean ("enabled"), GLib.CompareOperator.EQ, (int) enabled_before); } public void test_destroy () { var manager = new Ft.ActionManager (); var e = new DummyEventAction (null); e.display_name = "E"; e.command = new Ft.Command ("echo E"); e.event_names = {"start"}; var c = new DummyConditionAction (null); c.display_name = "C"; c.condition = new Ft.Variable ("is-running"); c.enter_command = new Ft.Command ("echo Enter"); c.exit_command = new Ft.Command ("echo Exit"); manager.model.save_action (e); manager.model.save_action (c); // Sanity: initially bound assert_cmpuint (e.bind_count, GLib.CompareOperator.EQ, 1U); assert_cmpuint (c.bind_count, GLib.CompareOperator.EQ, 1U); manager.destroy (); // Destroy should unbind actions assert_cmpuint (e.unbind_count, GLib.CompareOperator.GE, 1U); assert_cmpuint (c.unbind_count, GLib.CompareOperator.GE, 1U); // Toggling enabled should NOT sync to settings after destroy (handlers disconnected) var e_settings_enabled = e.settings.get_boolean ("enabled"); var c_settings_enabled = c.settings.get_boolean ("enabled"); e.enabled = !e_settings_enabled; c.enabled = !c_settings_enabled; assert_cmpint ((int) e.settings.get_boolean ("enabled"), GLib.CompareOperator.EQ, (int) e_settings_enabled); assert_cmpint ((int) c.settings.get_boolean ("enabled"), GLib.CompareOperator.EQ, (int) c_settings_enabled); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.EventActionTest (), new Tests.ConditionActionTest (), new Tests.ActionListModelTest (), new Tests.ActionManagerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-command.vala000066400000000000000000000332561520625676500232720ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private struct Case { public string line; public string[] expected_args; public int expected_error_code; } private inline void assert_command_error (GLib.Error error, int expected_error_code) { var error_domain = GLib.Quark.from_string ("ft-command-error-quark"); assert_error (error, error_domain, expected_error_code); } public class CommandTest : Tests.TestSuite { static Case[] BASE_CASES = { { "foo bar", { "foo", "bar" }, -1 }, { "foo 'bar'", { "foo", "bar" }, -1 }, { "foo \"bar\"", { "foo", "bar" }, -1 }, { "foo '' 'bar'", { "foo", "", "bar" }, -1 }, { "foo \"bar\"'baz'blah'foo'\\''blah'\"boo\"", { "foo", "barbazblahfoo'blahboo" }, -1 }, { "foo \t \tblah\tfoo\t\tbar baz", { "foo", "blah", "foo", "bar", "baz" }, -1 }, { "foo ' spaces more spaces lots of spaces in this ' \t", { "foo", " spaces more spaces lots of spaces in this " }, -1 }, { "foo \\\nbar", { "foo", "bar" }, -1 }, { "foo '' ''", { "foo", "", "" }, -1 }, { "foo \\\" la la la", { "foo", "\"", "la", "la", "la" }, -1 }, { "foo \\ foo woo woo\\ ", { "foo", " foo", "woo", "woo " }, -1 }, { "foo \"yada yada \\$\\\"\"", { "foo", "yada yada $\"" }, -1 }, { "foo \"c:\\\\\"", { "foo", "c:\\" }, -1 }, { "foo # comment\n bar", { "foo", "bar" }, -1 }, { "foo a#b", { "foo", "a#b" }, -1 }, { "foo '/bar/summer'\\''09 tours.pdf'", { "foo", "/bar/summer'09 tours.pdf" }, -1}, { "foo bar \"", { }, Ft.CommandError.SYNTAX_ERROR }, { "foo 'bar baz", { }, Ft.CommandError.SYNTAX_ERROR }, { "foo '\"bar\" baz", { }, Ft.CommandError.SYNTAX_ERROR }, { "foo bar \\", { }, Ft.CommandError.SYNTAX_ERROR }, { "", { }, Ft.CommandError.EMPTY_LINE }, { " ", { }, Ft.CommandError.EMPTY_LINE }, { "# comment", { }, Ft.CommandError.EMPTY_LINE }, }; static Case[] CASES_WITH_VARIABLES = { { "echo A${}B", { "echo", "AB" }, -1 }, { "echo A${:format}B", { "echo", "AB" }, -1 }, { "echo $", { "echo", "$" }, -1 }, { "echo $@", { "echo", "$@" }, -1 }, { "echo $$@", { "echo", "$$@" }, -1 }, { "echo $timestamp", { "echo", "1200000" }, -1 }, { "echo @$timestamp", { "echo", "@1200000" }, -1 }, { "echo $timestamp@", { "echo", "1200000@" }, -1 }, { "echo 時間$timestamp", { "echo", "時間1200000" }, -1 }, { "echo #comment: $timestamp", { "echo" }, -1 }, { "echo ${timestamp}", { "echo", "1200000" }, -1 }, { "echo ${timestamp:iso8601}", { "echo", "1970-01-01T00:00:01.200000Z" }, -1 }, { "echo ${timestamp:seconds}", { "echo", "1.2" }, -1 }, { "echo ${timestamp:microseconds}", { "echo", "1200000" }, -1 }, { "echo ${timestamp", { "echo", "${timestamp" }, -1 }, { "echo @${timestamp}", { "echo", "@1200000" }, -1 }, { "echo ${timestamp}@", { "echo", "1200000@" }, -1 }, // { "echo ${ timestamp }", { "echo", "1200000" }, -1 }, // TODO { "echo $state", { "echo", "short-break" }, -1 }, { "echo ${state}", { "echo", "short-break" }, -1 }, { "echo ${state:base}", { "echo", "break" }, -1 }, { "echo '$state:$timestamp'", { "echo", "short-break:1200000" }, -1 }, { "echo ${isRunning}", { "echo", "false" }, -1 }, }; private GLib.MainLoop? main_loop = null; private uint timeout_id = 0; public CommandTest () { this.add_test ("validate__empty_line", this.test_validate__empty_line); this.add_test ("validate__syntax_error", this.test_validate__syntax_error); this.add_test ("validate__not_found", this.test_validate__not_found); this.add_test ("validate__unknown_variable", this.test_validate__unknown_variable); this.add_test ("validate__unknown_variable_format", this.test_validate__unknown_variable_format); this.add_test ("prepare", this.test_prepare); this.add_test ("prepare__variables", this.test_prepare__variables); this.add_test ("prepare__use_subshell", this.test_prepare__use_subshell); this.add_test ("execute", this.test_execute); this.add_test ("execute__use_subshell", this.test_execute__use_subshell); this.add_test ("execute__working_directory", this.test_execute__working_directory); this.add_test ("execute__empty_line", this.test_execute__empty_line); } public override void setup () { this.main_loop = new GLib.MainLoop (); } public override void teardown () { this.main_loop = null; } private bool run_main_loop (uint timeout = 1000) { var success = true; if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } private void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } private Ft.CommandExecution? execute_sync (Ft.Command command, Ft.Context context) throws Ft.CommandError, GLib.Error { Ft.CommandExecution? execution = null; command.execute_async.begin ( context, (obj, res) => { execution = command.execute_async.end (res); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); if (execution != null && execution.error != null) { throw execution.error; } return (owned) execution; } public void test_validate__empty_line () { var command = new Ft.Command (""); Ft.CommandError? error = null; try { command.validate (); } catch (Ft.CommandError _error) { error = _error; } assert_command_error (error, Ft.CommandError.EMPTY_LINE); } public void test_validate__syntax_error () { var command = new Ft.Command ("echo \"unclosed"); Ft.CommandError? error = null; try { command.validate (); } catch (Ft.CommandError _error) { error = _error; } assert_command_error (error, Ft.CommandError.SYNTAX_ERROR); } public void test_validate__not_found () { var command = new Ft.Command ("@non-existing@"); Ft.CommandError? error = null; try { command.validate (); } catch (Ft.CommandError _error) { error = _error; } assert_command_error (error, Ft.CommandError.NOT_FOUND); } public void test_validate__unknown_variable () { var command = new Ft.Command ("echo ${invalid}"); Ft.CommandError? error = null; try { command.validate (); } catch (Ft.CommandError _error) { error = _error; } assert_command_error (error, Ft.CommandError.UNKNOWN_VARIABLE); } public void test_validate__unknown_variable_format () { var command = new Ft.Command ("echo ${timestamp:invalid}"); Ft.CommandError? error = null; try { command.validate (); } catch (Ft.CommandError _error) { error = _error; } assert_command_error (error, Ft.CommandError.UNKNOWN_VARIABLE_FORMAT); } private void test_prepare_case (Ft.Context context, string line, string[] expected_args, int expected_error_code) { var command = new Ft.Command (line); var execution = command.prepare (context); if (expected_error_code == Ft.CommandError.EMPTY_LINE) { assert_null (execution); } else { assert_nonnull (execution); if (expected_error_code < 0) { assert_no_error (execution.error); assert_cmpstrv (execution.args, expected_args); } else { assert_command_error (execution.error, expected_error_code); assert_cmpstrv (execution.args, { line }); } } } public void test_prepare () { var context = new Ft.Context (); foreach (var _case in BASE_CASES) { this.test_prepare_case (context, _case.line, _case.expected_args, _case.expected_error_code); } } public void test_prepare__variables () { var context = new Ft.Context (); context.timestamp = 1200000; context.timer_state = Ft.TimerState () { started_time = 1000000, paused_time = 1200000, }; context.time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); foreach (var _case in CASES_WITH_VARIABLES) { this.test_prepare_case (context, _case.line, _case.expected_args, _case.expected_error_code); } } public void test_prepare__use_subshell () { var context = new Ft.Context (); context.timestamp = 1200000; context.time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); var command = new Ft.Command ("echo ${state} && echo ${timestamp}"); command.use_subshell = true; var execution = command.prepare (context); assert_cmpstrv (execution.args, { "sh", "-c", "echo short-break && echo 1200000" }); assert_no_error (execution.error); } public void test_execute () { var command = new Ft.Command ("echo hello"); var context = new Ft.Context (); try { var execution = this.execute_sync (command, context); assert_nonnull (execution); assert_cmpint (execution.exit_code, GLib.CompareOperator.EQ, 0); assert_cmpstr (execution.output, GLib.CompareOperator.EQ, "hello\n"); assert_no_error (execution.error); } catch (GLib.Error error) { assert_no_error (error); } } public void test_execute__use_subshell () { var command = new Ft.Command ("cat <<< \"hello\""); command.use_subshell = true; var context = new Ft.Context (); try { var execution = this.execute_sync (command, context); assert_nonnull (execution); assert_cmpint (execution.exit_code, GLib.CompareOperator.EQ, 0); assert_cmpstr (execution.output, GLib.CompareOperator.EQ, "hello\n"); assert_no_error (execution.error); } catch (GLib.Error error) { assert_no_error (error); } } public void test_execute__working_directory () { var command = new Ft.Command ("pwd"); command.working_directory = "/tmp"; var context = new Ft.Context (); try { var execution = this.execute_sync (command, context); assert_nonnull (execution); assert_cmpint (execution.exit_code, GLib.CompareOperator.EQ, 0); assert_cmpstr (execution.output, GLib.CompareOperator.EQ, "/tmp\n"); assert_no_error (execution.error); } catch (GLib.Error error) { assert_no_error (error); } } public void test_execute__empty_line () { var command = new Ft.Command (""); var context = new Ft.Context (); try { var execution = this.execute_sync (command, context); assert_null (execution); } catch (GLib.Error error) { assert_no_error (error); } } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.CommandTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-cycle.vala000066400000000000000000000521121520625676500227430ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class CycleTest : Tests.TestSuite { public CycleTest () { this.add_test ("remove", this.test_remove); this.add_test ("get_weight", this.test_get_weight); this.add_test ("get_completion_time", this.test_get_completion_time); this.add_test ("get_completion_time__with_gaps", this.test_get_completion_time__with_gaps); this.add_test ("calculate_progress__empty", this.test_calculate_progress__empty); this.add_test ("calculate_progress__scheduled", this.test_calculate_progress__scheduled); this.add_test ("calculate_progress__in_progress", this.test_calculate_progress__in_progress); this.add_test ("calculate_progress__completed", this.test_calculate_progress__completed); this.add_test ("calculate_progress__uncompleted", this.test_calculate_progress__uncompleted); this.add_test ("calculate_progress__with_gaps", this.test_calculate_progress__with_gaps); this.add_test ("is_visible__empty", this.test_is_visible__empty); this.add_test ("is_visible__only_uncompleted", this.test_is_visible__only_uncompleted); this.add_test ("is_visible__only_zero_weight", this.test_is_visible__only_zero_weight); this.add_test ("is_visible__scheduled", this.test_is_visible__scheduled); this.add_test ("is_visible__in_progress", this.test_is_visible__in_progress); this.add_test ("is_visible__completed", this.test_is_visible__completed); this.add_test ("is_visible__mixed_uncompleted_then_scheduled", this.test_is_visible__mixed_uncompleted_then_scheduled); this.add_test ("is_visible__mixed_zero_weight_then_weighted", this.test_is_visible__mixed_zero_weight_then_weighted); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); } public override void teardown () { Ft.Timestamp.thaw (); var settings = Ft.get_settings (); settings.revert (); } public void test_remove () { var removed_emitted = 0; var weak_notify_emitted = 0; var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.weak_ref (() => { weak_notify_emitted++; }); var cycle = new Ft.Cycle (); cycle.append (time_block); cycle.removed.connect (() => { removed_emitted++; }); assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 2); cycle.remove (time_block); assert_false (cycle.contains (time_block)); assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 1); time_block = null; assert_cmpuint (removed_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (weak_notify_emitted, GLib.CompareOperator.EQ, 1); } public void test_get_weight () { var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 1.0, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0, } ); var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_3.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); assert_cmpfloat_with_epsilon (cycle.get_weight (), 0.0, 0.0001); cycle.append (time_block_2); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, 0.0001); cycle.append (time_block_3); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, 0.0001); } public void test_get_completion_time () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 1.0, completion_time = now + 20 * Ft.Interval.MINUTE, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0, completion_time = now + 25 * Ft.Interval.MINUTE, } ); var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_3.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); assert_cmpvariant ( new GLib.Variant.int64 (cycle.get_completion_time ()), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); cycle.append (time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (cycle.get_completion_time ()), new GLib.Variant.int64 (now + 25 * Ft.Interval.MINUTE) ); cycle.append (time_block_3); assert_cmpvariant ( new GLib.Variant.int64 (cycle.get_completion_time ()), new GLib.Variant.int64 (now + 25 * Ft.Interval.MINUTE) ); } public void test_get_completion_time__with_gaps () { var now = Ft.Timestamp.peek (); var gap = new Ft.Gap (); gap.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 7 * Ft.Interval.MINUTE); // 2 minutes var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.IN_PROGRESS, weight = 1.0, intended_duration = 25 * Ft.Interval.MINUTE, completion_time = now + 22 * Ft.Interval.MINUTE, } ); time_block_1.add_gap (gap); time_block_1.set_time_range (now, now + 27 * Ft.Interval.MINUTE); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (cycle.get_completion_time ()), new GLib.Variant.int64 (now + 22 * Ft.Interval.MINUTE) ); } public void test_calculate_progress__empty () { var now = Ft.Timestamp.peek (); var cycle = new Ft.Cycle (); assert_cmpfloat ( cycle.calculate_progress (now), GLib.CompareOperator.EQ, 0.0 ); } public void test_calculate_progress__scheduled () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0, completion_time = now + 20 * Ft.Interval.MINUTE, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); assert_cmpfloat ( cycle.calculate_progress (time_block_1.start_time), GLib.CompareOperator.EQ, 0.0 ); assert_cmpfloat ( cycle.calculate_progress (time_block_1.end_time), GLib.CompareOperator.EQ, 0.0 ); assert_cmpfloat ( cycle.calculate_progress (time_block_2.end_time), GLib.CompareOperator.EQ, 0.0 ); } public void test_calculate_progress__in_progress () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 1.0, completion_time = now + 20 * Ft.Interval.MINUTE, intended_duration = 25 * Ft.Interval.MINUTE, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.IN_PROGRESS, weight = 1.0, completion_time = now + 25 * Ft.Interval.MINUTE, intended_duration = 25 * Ft.Interval.MINUTE, } ); var time_block_3 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_3.set_time_range (now + 30 * Ft.Interval.MINUTE, now + 35 * Ft.Interval.MINUTE); time_block_3.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, intended_duration = 5 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); cycle.append (time_block_3); assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_1.end_time), 0.0, 0.0001); assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.start_time + 5 * Ft.Interval.MINUTE), 0.25, 0.0001); assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.end_time), 1.0, 0.0001); assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_3.end_time), 1.0, 0.0001); } public void test_calculate_progress__completed () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.COMPLETED, weight = 1.0, completion_time = now + 20 * Ft.Interval.MINUTE, intended_duration = 25 * Ft.Interval.MINUTE, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.COMPLETED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, intended_duration = 5 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); assert_cmpfloat_with_epsilon (cycle.calculate_progress (time_block_2.end_time), 1.0, 0.0001); } public void test_calculate_progress__uncompleted () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 10 * Ft.Interval.MINUTE); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 0.0, completion_time = now + 20 * Ft.Interval.MINUTE, intended_duration = 25 * Ft.Interval.MINUTE, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (now + 10 * Ft.Interval.MINUTE, now + 15 * Ft.Interval.MINUTE); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.IN_PROGRESS, weight = 0.0, completion_time = now + 14 * Ft.Interval.MINUTE, intended_duration = 5 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); assert_cmpfloat ( cycle.calculate_progress (time_block_2.end_time), GLib.CompareOperator.EQ, 0.0 ); assert_cmpfloat ( cycle.calculate_progress (time_block_2.end_time), GLib.CompareOperator.EQ, 0.0 ); } public void test_calculate_progress__with_gaps () { var now = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.IN_PROGRESS, weight = 1.0, completion_time = now + 20 * Ft.Interval.MINUTE, intended_duration = 25 * Ft.Interval.MINUTE, } ); var gap = new Ft.Gap (); gap.set_time_range (now + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block_1.add_gap (gap); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (now + 25 * Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, completion_time = now + 29 * Ft.Interval.MINUTE, intended_duration = 5 * Ft.Interval.MINUTE, } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); assert_cmpfloat_with_epsilon ( cycle.calculate_progress (gap.start_time + Ft.Interval.MINUTE), 0.25, 0.0001); } public void test_is_visible__empty () { var cycle = new Ft.Cycle (); assert_false (cycle.is_visible ()); } public void test_is_visible__only_uncompleted () { var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 1.0, } ); var cycle = new Ft.Cycle (); cycle.append (time_block); assert_false (cycle.is_visible ()); } public void test_is_visible__only_zero_weight () { var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 0.0, } ); var cycle = new Ft.Cycle (); cycle.append (time_block); assert_false (cycle.is_visible ()); } public void test_is_visible__scheduled () { var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0 } ); var cycle = new Ft.Cycle (); cycle.append (time_block); assert_true (cycle.is_visible ()); } public void test_is_visible__in_progress () { var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.IN_PROGRESS, weight = 1.0 } ); var cycle = new Ft.Cycle (); cycle.append (time_block); assert_true (cycle.is_visible ()); } public void test_is_visible__completed () { var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.COMPLETED, weight = 1.0 } ); var cycle = new Ft.Cycle (); cycle.append (time_block); assert_true (cycle.is_visible ()); } public void test_is_visible__mixed_uncompleted_then_scheduled () { var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.UNCOMPLETED, weight = 1.0, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0 } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); // Should be visible because second block is scheduled with weight > 0 assert_true (cycle.is_visible ()); } public void test_is_visible__mixed_zero_weight_then_weighted () { var time_block_1 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_1.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.COMPLETED, weight = 0.0, } ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_meta ( Ft.TimeBlockMeta () { status = Ft.TimeBlockStatus.SCHEDULED, weight = 1.0 } ); var cycle = new Ft.Cycle (); cycle.append (time_block_1); cycle.append (time_block_2); // Should be visible because second block has weight > 0 and is scheduled assert_true (cycle.is_visible ()); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.CycleTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-database.vala000066400000000000000000000205361520625676500234150ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ using GLib; namespace Tests { public class DatabaseMigrationTest : Tests.TestSuite { private Gom.Adapter? adapter = null; private Gom.Repository? repository = null; private GLib.MainLoop? main_loop = null; private uint timeout_id = 0; public DatabaseMigrationTest () { this.add_test ("migrate_to_v3", this.test_migrate_to_v3); } public override void setup () { this.main_loop = new GLib.MainLoop (); this.adapter = new Gom.Adapter (); try { adapter.open_sync (":memory:"); this.repository = new Gom.Repository (adapter); } catch (GLib.Error error) { assert_no_error (error); } } public override void teardown () { try { this.adapter.close_sync (); } catch (GLib.Error error) { GLib.warning ("Error while closing database: %s", error.message); } this.main_loop = null; this.adapter = null; this.repository = null; } private bool run_main_loop (uint timeout = 1000) { var success = true; if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } private void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } private bool table_exists (string table_name) { var exists = true; this.adapter.queue_read (() => { try { this.adapter.execute_sql ("SELECT 1 FROM '" + table_name + "' LIMIT 1;"); exists = true; } catch (GLib.Error error) { exists = false; } GLib.Idle.add (() => { this.quit_main_loop (); return GLib.Source.REMOVE; }); }); assert_true (this.run_main_loop ()); return exists; } public void test_migrate_to_v3 () { // Apply v1 and v2 migrations try { this.repository.migrate_sync (2U, Ft.Database.migrate_repository); } catch (GLib.Error error) { assert_no_error (error); } // Insert legacy data into entries (durations in seconds) this.adapter.queue_write (() => { try { // 2000-01-01 03:00:00 (UTC and local), duration 120s, pomodoro this.adapter.execute_sql ( "INSERT INTO \"entries\" (\"datetime-string\", \"datetime-local-string\", \"state-name\", \"state-duration\", \"elapsed\") " + "VALUES ('2000-01-01 03:00:00', '2000-01-01 03:00:00', 'pomodoro', 120, 120);"); // 2000-01-01 12:34:56 (UTC and local), duration 300s, short-break -> break this.adapter.execute_sql ( "INSERT INTO \"entries\" (\"datetime-string\", \"datetime-local-string\", \"state-name\", \"state-duration\", \"elapsed\") " + "VALUES ('2000-01-01 12:34:56', '2000-01-01 12:34:56', 'short-break', 300, 300);"); } catch (GLib.Error error) { GLib.critical ("%s", error.message); } GLib.Idle.add (() => { this.quit_main_loop (); return GLib.Source.REMOVE; }); }); assert_true (this.run_main_loop ()); // Apply v3 migration (populate stats and drop legacy tables) try { repository.migrate_sync (3U, Ft.Database.migrate_repository); } catch (GLib.Error error) { assert_no_error (error); } // Verify stats entries try { var results = repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); // Find pomodoro entry Ft.StatsEntry? pomodoro_entry = null; Ft.StatsEntry? break_entry = null; for (var index = 0; index < results.count; index++) { var entry = (Ft.StatsEntry?) results.get_index (index); if (entry.category == "pomodoro") { pomodoro_entry = entry; } else if (entry.category == "break") { break_entry = entry; } } assert_nonnull (pomodoro_entry); assert_nonnull (break_entry); // time = epoch micros of UTC datetime-string assert_cmpstr (pomodoro_entry.date, GLib.CompareOperator.EQ, "1999-12-31"); assert_cmpvariant (new GLib.Variant.int64 (pomodoro_entry.offset), new GLib.Variant.int64 ((24 + 3) * Ft.Interval.HOUR)); assert_cmpvariant (new GLib.Variant.int64 (pomodoro_entry.duration), new GLib.Variant.int64 (120 * Ft.Interval.SECOND)); assert_cmpstr (break_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant (new GLib.Variant.int64 (break_entry.offset), new GLib.Variant.int64 (12 * Ft.Interval.HOUR + 34 * Ft.Interval.MINUTE + 56 * Ft.Interval.SECOND)); assert_cmpvariant (new GLib.Variant.int64 (break_entry.duration), new GLib.Variant.int64 (300 * Ft.Interval.SECOND)); // Aggregated stats should also be updated by triggers var agg_results = repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); agg_results.fetch_sync (0U, agg_results.count); var found_pomodoro_agg = false; var found_break_agg = false; for (var index = 0; index < agg_results.count; index++) { var aggregated_entry = (Ft.AggregatedStatsEntry?) agg_results.get_index (index); if (aggregated_entry.category == "pomodoro") { found_pomodoro_agg = true; assert_cmpstr (aggregated_entry.date, GLib.CompareOperator.EQ, "1999-12-31"); assert_cmpvariant (new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (120 * Ft.Interval.SECOND)); } else if (aggregated_entry.category == "break") { found_break_agg = true; assert_cmpstr (aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant (new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (300 * Ft.Interval.SECOND)); } } assert_true (found_pomodoro_agg); assert_true (found_break_agg); } catch (GLib.Error error) { assert_no_error (error); } // Verify legacy tables are dropped assert_false (this.table_exists ("entries")); assert_false (this.table_exists ("aggregated-entries")); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.DatabaseMigrationTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-event-bus.vala000066400000000000000000000244241520625676500235610ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class EventBusTest : Tests.TestSuite { private Ft.Timer timer; private Ft.SessionManager session_manager; private Ft.EventProducer producer; private Ft.EventBus bus; private Ft.SessionManagerActionGroup session_manager_action_group; private Ft.TimerActionGroup timer_action_group; public EventBusTest () { this.add_test ("add_event_watch__start", this.test_add_event_watch__start); this.add_test ("add_event_watch__pause", this.test_add_event_watch__pause); this.add_test ("add_event_watch__with_condition", this.test_add_event_watch__with_condition); this.add_test ("remove_event_watch", this.test_remove_event_watch); this.add_test ("add_condition_watch", this.test_add_condition_watch); this.add_test ("destroy", this.test_destroy); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); this.session_manager = new Ft.SessionManager.with_timer (this.timer); Ft.SessionManager.set_default (this.session_manager); this.session_manager_action_group = new Ft.SessionManagerActionGroup (); assert (this.session_manager_action_group.session_manager == this.session_manager); this.timer_action_group = new Ft.TimerActionGroup (); assert (this.timer_action_group.timer == this.timer); this.producer = new Ft.EventProducer (); assert (producer.session_manager == this.session_manager); assert (producer.timer == this.timer); this.bus = this.producer.bus; } public override void teardown () { this.producer = null; this.bus = null; this.session_manager = null; this.timer = null; Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); Ft.Context.unset_event_source (); var settings = Ft.get_settings (); settings.revert (); } /* * Events */ public void test_add_event_watch__start () { var expected_timestamp = Ft.Timestamp.from_now (); var event_triggered_count = 0; this.bus.add_event_watch ("start", null, (event) => { event_triggered_count++; assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("start", null); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 1); } public void test_add_event_watch__pause () { this.timer.start (); var expected_timestamp = Ft.Timestamp.from_now (); var event_triggered_count = 0; this.bus.add_event_watch ("pause", null, (event) => { event_triggered_count++; assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("pause", null); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 1); } public void test_add_event_watch__with_condition () { var condition = new Ft.Comparison ( new Ft.Variable ("state"), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.BREAK)) ); var event_triggered_count = 0; this.bus.add_event_watch ("pause", condition, (event) => { event_triggered_count++; assert_true (event.context.timer_state.is_paused ()); }); this.session_manager.advance_to_state (Ft.State.POMODORO); // Make condition unmet. this.timer_action_group.activate_action ("pause", null); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 0); // Make condition met. this.session_manager_action_group.activate_action ("advance", null); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 0); assert_false (this.timer.is_paused ()); this.timer_action_group.activate_action ("pause", null); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 1); } public void test_remove_event_watch () { var expected_refcount = this.ref_count; var event_triggered_count = 0; var watch_id = this.bus.add_event_watch ("start", null, (event) => { event_triggered_count++; }); this.bus.remove_event_watch (watch_id); this.timer.start (); assert_cmpuint (event_triggered_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (this.ref_count, GLib.CompareOperator.EQ, expected_refcount); } /* * Conditions */ public void test_add_condition_watch () { var start_timestamp = Ft.Timestamp.peek (); var pause_timestamp = start_timestamp + Ft.Interval.MINUTE; var stop_timestamp = pause_timestamp + Ft.Interval.MINUTE; var signals = new string[0]; var paused_called = false; var resumed_called = false; this.bus.add_condition_watch ( new Ft.Comparison.is_true (new Ft.Variable ("is-started")), (context) => { signals += "enter-condition"; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (start_timestamp) ); }, (context) => { signals += "leave-condition"; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (stop_timestamp) ); }); this.bus.add_condition_watch ( new Ft.Comparison.is_true (new Ft.Variable ("is-paused")), (context) => { paused_called = true; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (pause_timestamp) ); }, (context) => { resumed_called = true; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (stop_timestamp) ); }); Ft.Timestamp.freeze_to (start_timestamp); this.timer.start (); Ft.Timestamp.freeze_to (pause_timestamp); this.timer.pause (); Ft.Timestamp.freeze_to (stop_timestamp); this.timer.reset (); assert_cmpstrv (signals, { "enter-condition", "leave-condition" }); assert_true (paused_called); assert_true (resumed_called); } /** * Test that destroy calls leave on active watches. */ public void test_destroy () { var start_timestamp = Ft.Timestamp.peek (); var destroy_timestamp = start_timestamp + Ft.Interval.MINUTE; var signals = new string[0]; this.bus.add_condition_watch ( new Ft.Comparison.is_true (new Ft.Variable ("is-started")), (context) => { signals += "enter-condition"; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (start_timestamp) ); }, (context) => { signals += "leave-condition"; assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (destroy_timestamp) ); }); // Activate the condition so the watch becomes active. Ft.Timestamp.freeze_to (start_timestamp); this.timer.start (); // Destroy should call leave on active watches with current context. Ft.Timestamp.freeze_to (destroy_timestamp); this.bus.destroy (); // Ensure no duplicate leave after a subsequent reset. var after_destroy_timestamp = destroy_timestamp + Ft.Interval.SECOND; Ft.Timestamp.freeze_to (after_destroy_timestamp); this.timer.reset (); assert_cmpstrv (signals, { "enter-condition", "leave-condition" }); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.EventBusTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-event-producer.vala000066400000000000000000000633131520625676500246130ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class EventProducerTest : Tests.TestSuite { private Ft.Timer timer; private Ft.SessionManager session_manager; private Ft.EventProducer producer; private Ft.SessionManagerActionGroup session_manager_action_group; private Ft.TimerActionGroup timer_action_group; public EventProducerTest () { this.add_test ("timer_start", this.test_timer_start); this.add_test ("timer_reset", this.test_timer_reset); this.add_test ("timer_reset__paused", this.test_timer_reset__paused); this.add_test ("timer_pause", this.test_timer_pause); this.add_test ("timer_resume", this.test_timer_resume); this.add_test ("timer_rewind", this.test_timer_rewind); this.add_test ("timer_finished__continuous", this.test_timer_finished__continuous); this.add_test ("timer_finished__wait_for_activity", this.test_timer_finished__wait_for_activity); this.add_test ("timer_finished__manual", this.test_timer_finished__manual); this.add_test ("session_manager_advance__uncompleted", this.test_session_manager_advance__completed); this.add_test ("session_manager_advance__completed", this.test_session_manager_advance__uncompleted); this.add_test ("session_manager_advance__uncompleted_paused_pomodoro", this.test_session_manager_advance__uncompleted_paused_pomodoro); this.add_test ("session_manager_advance__completed_paused_pomodoro", this.test_session_manager_advance__completed_paused_pomodoro); this.add_test ("session_manager_confirm_advancement", this.test_session_manager_confirm_advancement); this.add_test ("session_manager_session_expired", this.test_session_manager_session_expired); this.add_test ("session_manager_reset", this.test_session_manager_reset); this.add_test ("session_manager_ensure_session", this.test_session_manager_ensure_session); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); this.session_manager = new Ft.SessionManager.with_timer (this.timer); Ft.SessionManager.set_default (this.session_manager); this.producer = new Ft.EventProducer (); assert (producer.session_manager == this.session_manager); assert (producer.timer == this.timer); this.session_manager_action_group = new Ft.SessionManagerActionGroup (); assert (this.session_manager_action_group.session_manager == this.session_manager); this.timer_action_group = new Ft.TimerActionGroup (); assert (this.timer_action_group.timer == this.timer); } public override void teardown () { this.producer = null; this.session_manager_action_group = null; this.timer_action_group = null; Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); Ft.Context.unset_event_source (); var settings = Ft.get_settings (); settings.revert (); } /* * Timer */ public void test_timer_start () { this.session_manager.ensure_session (); var time_block = this.session_manager.current_session.get_first_time_block (); var expected_timestamp = Ft.Timestamp.from_now (); var expected_state = Ft.TimerState () { duration = time_block.duration, started_time = expected_timestamp, user_data = (void*) time_block }; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_nonnull (event.context.time_block); assert_true (event.context.time_block.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("start", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "start", "change", }); } public void test_timer_reset () { this.timer.start (); var expected_timestamp = Ft.Timestamp.from_now (); var expected_state = Ft.TimerState (); var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_null (event.context.time_block); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("reset", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "stop", "change" }); } public void test_timer_reset__paused () { this.timer.start (); this.timer.pause (); var expected_timestamp = this.timer.state.paused_time + Ft.Interval.MINUTE; var expected_state = Ft.TimerState (); var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_null (event.context.time_block); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("reset", null); // Expect "resume" event not to be triggered. assert_cmpstrv (event_names, { "reschedule", "state-change", "stop", "change" }); } public void test_timer_pause () { this.timer.start (); var expected_timestamp = this.timer.state.started_time + Ft.Interval.MINUTE; var expected_state = this.timer.state; expected_state.paused_time = expected_timestamp; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("pause", null); assert_cmpstrv (event_names, { "pause", "change" }); } public void test_timer_resume () { this.timer.start (); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); this.timer.pause (); var expected_timestamp = this.timer.state.paused_time + Ft.Interval.MINUTE; var expected_state = this.timer.state; expected_state.offset = Ft.Interval.MINUTE; expected_state.paused_time = Ft.Timestamp.UNDEFINED; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("resume", null); assert_cmpstrv (event_names, { "reschedule", "resume", "change" }); } public void test_timer_rewind () { this.timer.start (); var expected_timestamp = this.timer.state.started_time + 5 * Ft.Interval.MINUTE; var expected_state = this.timer.state; expected_state.offset = Ft.Interval.MINUTE; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( event.context.timer_state.to_variant (), expected_state.to_variant () ); assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); }); Ft.Timestamp.freeze_to (expected_timestamp); this.timer_action_group.activate_action ("rewind", null); assert_cmpstrv (event_names, { "reschedule", "rewind", "change" }); } public void test_timer_finished__continuous () { this.timer.start (); var finished_time = this.session_manager.current_time_block.end_time; var expected_timestamp = finished_time; var event_names = new string[0]; var finished = false; var expected_state_1 = this.timer.state; expected_state_1.finished_time = finished_time; var time_block_2 = this.session_manager.current_session.get_nth_time_block (1); var expected_state_2 = Ft.TimerState () { duration = time_block_2.duration, started_time = finished_time, user_data = time_block_2 }; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); if (!finished) { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_1.to_variant ()); } else { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_2.to_variant ()); } if (event.spec.name == "finish") { finished = true; } }); Ft.Timestamp.freeze_to (finished_time); this.timer.finish (); assert_cmpstrv (event_names, { "finish", "state-change", "change", "advance" }); } public void test_timer_finished__wait_for_activity () { var idle_monitor = new Ft.IdleMonitor (); assert_true (idle_monitor.provider is Ft.DummyIdleMonitorProvider); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); var finished_time = this.session_manager.current_time_block.end_time; var activity_time = finished_time + Ft.Interval.MINUTE; var expected_timestamp = finished_time; var event_names = new string[0]; var finished = false; var became_active = false; var time_block_1 = this.session_manager.current_time_block; var expected_state_1 = this.timer.state; expected_state_1.finished_time = finished_time; var time_block_2 = this.session_manager.current_session.get_next_time_block (time_block_1); var expected_state_2 = Ft.TimerState () { duration = time_block_2.duration, started_time = Ft.Timestamp.UNDEFINED, user_data = time_block_2 }; var expected_state_3 = expected_state_2; expected_state_3.started_time = activity_time; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); if (!finished) { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_1.to_variant ()); } else if (!became_active) { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_2.to_variant ()); } else { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_3.to_variant ()); } if (event.spec.name == "finish") { finished = true; } }); Ft.Timestamp.freeze_to (finished_time); this.timer.finish (); assert_cmpstrv (event_names, { "finish", "state-change", "change", "advance" }); // Simulate inactivity of 1 minute. became_active = true; expected_timestamp = activity_time; event_names = {}; Ft.Timestamp.freeze_to (activity_time); idle_monitor.provider.became_active (); assert_cmpstrv (event_names, { "reschedule", "change" }); } public void test_timer_finished__manual () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); settings.set_boolean ("confirm-starting-pomodoro", true); this.timer.start (); var finished_time = this.session_manager.current_time_block.end_time; var confirmed_time = finished_time + Ft.Interval.MINUTE; var expected_timestamp = finished_time; var event_names = new string[0]; var confirmed = false; var time_block_1 = this.session_manager.current_time_block; var expected_state_1 = this.timer.state; expected_state_1.finished_time = finished_time; var time_block_2 = this.session_manager.current_session.get_next_time_block (time_block_1); var expected_state_2 = Ft.TimerState () { duration = time_block_2.duration, started_time = confirmed_time, user_data = time_block_2 }; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; assert_cmpvariant ( new GLib.Variant.int64 (event.context.timestamp), new GLib.Variant.int64 (expected_timestamp) ); if (!confirmed) { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_1.to_variant ()); } else { assert_cmpvariant (event.context.timer_state.to_variant (), expected_state_2.to_variant ()); } }); Ft.Timestamp.freeze_to (finished_time); this.timer.finish (); assert_cmpstrv (event_names, { "finish", "confirm-advancement" }); // Confirm after 1 minute. confirmed = true; expected_timestamp = confirmed_time; event_names = {}; Ft.Timestamp.freeze_to (confirmed_time); this.session_manager_action_group.activate_action ("advance", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "advance" }); } /* * SessionManager */ public void test_session_manager_advance__uncompleted () { this.session_manager.advance_to_state (Ft.State.POMODORO); var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; // TODO: check context }); Ft.Timestamp.advance (Ft.Interval.MINUTE); this.session_manager_action_group.activate_action ("advance", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "skip", "advance" }); } public void test_session_manager_advance__completed () { this.session_manager.advance_to_state (Ft.State.POMODORO); var expected_timestamp = this.session_manager.current_time_block.end_time - Ft.Interval.MINUTE; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; // TODO: check context }); Ft.Timestamp.freeze_to (expected_timestamp); this.session_manager_action_group.activate_action ("advance", null); // Expect no "skip" event, because time-block got completed assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "advance" }); } public void test_session_manager_advance__uncompleted_paused_pomodoro () { this.session_manager.advance_to_state (Ft.State.POMODORO); Ft.Timestamp.advance (Ft.Interval.MINUTE); this.timer.pause (); var expected_timestamp = Ft.Timestamp.peek () + Ft.Interval.MINUTE; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; }); Ft.Timestamp.freeze_to (expected_timestamp); this.session_manager_action_group.activate_action ("advance", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "skip", "advance" }); } public void test_session_manager_advance__completed_paused_pomodoro () { this.session_manager.advance_to_state (Ft.State.POMODORO); Ft.Timestamp.advance (24 * Ft.Interval.MINUTE); this.timer.pause (); var expected_timestamp = Ft.Timestamp.peek () + Ft.Interval.MINUTE; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; }); Ft.Timestamp.freeze_to (expected_timestamp); this.session_manager_action_group.activate_action ("advance", null); // Expect no "skip" event, because time-block got completed assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "advance" }); } public void test_session_manager_confirm_advancement () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); settings.set_boolean ("confirm-starting-pomodoro", true); this.session_manager.advance_to_state (Ft.State.POMODORO); var finished_time = this.session_manager.current_time_block.end_time; var confirmed_time = finished_time + Ft.Interval.MINUTE; var expected_timestamp = finished_time; var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; // TODO: check context }); Ft.Timestamp.freeze_to (finished_time); this.timer.finish (); assert_cmpstrv (event_names, { "finish", "confirm-advancement" }); // Confirm after 1 minute. expected_timestamp = confirmed_time; event_names = {}; Ft.Timestamp.freeze_to (confirmed_time); this.session_manager_action_group.activate_action ("advance", null); assert_cmpstrv (event_names, { "reschedule", "state-change", "change", "advance" }); } public void test_session_manager_session_expired () { this.timer.start (); this.timer.pause (); var event_names = new string[0]; var session_expired_emitted = 0; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; // TODO: check context }); this.session_manager.session_expired.connect (() => { session_expired_emitted++; }); Ft.Timestamp.advance (Ft.SessionManager.SESSION_EXPIRY_TIMEOUT + Ft.Interval.MINUTE); this.session_manager.check_current_session_expired (); assert_cmpuint (session_expired_emitted, GLib.CompareOperator.EQ, 1); assert_cmpstrv (event_names, { "expire", "reschedule", "state-change", "change", }); } public void test_session_manager_reset () { this.timer.start (); Ft.Timestamp.advance (Ft.Interval.MINUTE); this.timer.reset (); var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; }); this.session_manager_action_group.activate_action ("reset", null); assert_cmpstrv (event_names, { "reschedule", // XXX: should be after the reset "reset" }); } public void test_session_manager_ensure_session () { assert_null (this.session_manager.current_session); var event_names = new string[0]; this.producer.flush (); this.producer.event.connect ( (event) => { event_names += event.spec.name; }); this.session_manager.ensure_session (); // Expect no events. "reschedule" will be emitted on timer start. assert_cmpstrv (event_names, {}); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.EventProducerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-expression-parser.vala000066400000000000000000000604661520625676500253500ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private inline void assert_value_equals (Ft.Value? value, Ft.Value expected_value) { assert_nonnull (value); assert_cmpstr ( value.get_type_name (), GLib.CompareOperator.EQ, expected_value.get_type_name ()); assert_cmpvariant ( value.to_variant (), expected_value.to_variant ()); } private inline void assert_root_has_operator (Ft.Expression expression, Ft.Operator operator) { if (expression is Ft.Operation) { var operation = (Ft.Operation) expression; assert_cmpstr (operation.operator.to_string (), GLib.CompareOperator.EQ, operator.to_string ()); return; } if (expression is Ft.Comparison) { var comparison = (Ft.Comparison) expression; assert_cmpstr (comparison.operator.to_string (), GLib.CompareOperator.EQ, operator.to_string ()); return; } assert_not_reached (); } public class ExpressionParserTest : Tests.TestSuite { private GLib.Quark error_domain; public ExpressionParserTest () { this.add_test ("parse_empty", this.test_parse_empty); this.add_test ("parse_string_literal__string", this.test_parse_string_literal__string); this.add_test ("parse_string_literal__state", this.test_parse_string_literal__state); this.add_test ("parse_string_literal__status", this.test_parse_string_literal__status); this.add_test ("parse_string_literal__timestamp", this.test_parse_string_literal__timestamp); this.add_test ("parse_string_literal__syntax_error", this.test_parse_string_literal__syntax_error); this.add_test ("parse_numeric_literal__interval", this.test_parse_numeric_literal__interval); this.add_test ("parse_numeric_literal__syntax_error", this.test_parse_numeric_literal__syntax_error); this.add_test ("parse_identifier__variable", this.test_parse_identifier__variable); this.add_test ("parse_identifier__boolean", this.test_parse_identifier__boolean); this.add_test ("parse_identifier__syntax_error", this.test_parse_identifier__syntax_error); this.add_test ("parse_identifier__unknown_identifier_error", this.test_parse_identifier__unknown_identifier_error); this.add_test ("parse_operator__or", this.test_parse_operator__or); this.add_test ("parse_operator__and", this.test_parse_operator__and); this.add_test ("parse_operator__eq", this.test_parse_operator__eq); this.add_test ("parse_operator__gt", this.test_parse_operator__gt); this.add_test ("parse_parentheses", this.test_parse_parentheses); this.add_test ("parse_parentheses__syntax_error", this.test_parse_parentheses__syntax_error); this.add_test ("precedence__logical_operator", this.test_precedence__logical_operator); this.add_test ("precedence__comparison_operator", this.test_precedence__comparison_operator); this.add_test ("syntax_error", this.test_syntax_error); } public override void setup () { this.error_domain = GLib.Quark.from_string ("ft-expression-parser-error-quark"); } public void test_parse_empty () { try { assert_null (Ft.Expression.parse ("")); assert_null (Ft.Expression.parse (" ")); assert_null (Ft.Expression.parse ("\n")); assert_null (Ft.Expression.parse ("\t")); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } /* * String Literal */ public void test_parse_string_literal__string () { try { var expression_1 = Ft.Expression.parse ("\"hello\""); var expression_2 = Ft.Expression.parse (" \"hello\" "); var expression_3 = Ft.Expression.parse ("\"a\nb\""); var expression_4 = Ft.Expression.parse (""" "a\"b" """); assert_cmpstr ((expression_1 as Ft.Constant)?.get_string (), GLib.CompareOperator.EQ, "hello"); assert_cmpstr ((expression_2 as Ft.Constant)?.get_string (), GLib.CompareOperator.EQ, "hello"); assert_cmpstr ((expression_3 as Ft.Constant)?.get_string (), GLib.CompareOperator.EQ, "a\nb"); assert_cmpstr ((expression_4 as Ft.Constant)?.get_string (), GLib.CompareOperator.EQ, "a\"b"); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_string_literal__state () { try { var expression_1 = Ft.Expression.parse ("\"pomodoro\"") as Ft.Constant; assert_value_equals (expression_1?.value, new Ft.StateValue (Ft.State.POMODORO)); var expression_2 = Ft.Expression.parse ("\"break\"") as Ft.Constant; assert_value_equals (expression_2?.value, new Ft.StateValue (Ft.State.BREAK)); var expression_3 = Ft.Expression.parse ("\"short-break\"") as Ft.Constant; assert_value_equals (expression_3?.value, new Ft.StateValue (Ft.State.SHORT_BREAK)); var expression_4 = Ft.Expression.parse ("\"long-break\"") as Ft.Constant; assert_value_equals (expression_4?.value, new Ft.StateValue (Ft.State.LONG_BREAK)); var expression_5 = Ft.Expression.parse ("\"stopped\"") as Ft.Constant; assert_value_equals (expression_5?.value, new Ft.StateValue (Ft.State.STOPPED)); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_string_literal__status () { try { var expression_1 = Ft.Expression.parse ("\"scheduled\"") as Ft.Constant; assert_value_equals (expression_1?.value, new Ft.StatusValue (Ft.TimeBlockStatus.SCHEDULED)); var expression_2 = Ft.Expression.parse ("\"in-progress\"") as Ft.Constant; assert_value_equals (expression_2?.value, new Ft.StatusValue (Ft.TimeBlockStatus.IN_PROGRESS)); var expression_3 = Ft.Expression.parse ("\"completed\"") as Ft.Constant; assert_value_equals (expression_3?.value, new Ft.StatusValue (Ft.TimeBlockStatus.COMPLETED)); var expression_4 = Ft.Expression.parse ("\"uncompleted\"") as Ft.Constant; assert_value_equals (expression_4?.value, new Ft.StatusValue (Ft.TimeBlockStatus.UNCOMPLETED)); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_string_literal__timestamp () { try { var expected_timestamp = Ft.Timestamp.from_seconds_uint (1014304205); var expression = Ft.Expression.parse ("\"2002-02-21T15:10:05Z\"") as Ft.Constant; assert_value_equals (expression?.value, new Ft.TimestampValue (expected_timestamp)); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_string_literal__syntax_error () { string[] invalid_texts = { "\"hello", " \"hello\" \"world\" ", "'hello'" }; foreach (var text in invalid_texts) { try { Ft.Expression.parse (text); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.SYNTAX_ERROR); } } } /* * Numeric Literal / Intervals */ public void test_parse_numeric_literal__interval () { try { var expression_1 = Ft.Expression.parse ("1") as Ft.Constant; assert_value_equals (expression_1?.value, new Ft.IntervalValue (Ft.Interval.MICROSECOND)); var expression_2 = Ft.Expression.parse ("0") as Ft.Constant; assert_value_equals (expression_2?.value, new Ft.IntervalValue (0)); var expression_3 = Ft.Expression.parse ("1000000") as Ft.Constant; assert_value_equals (expression_3?.value, new Ft.IntervalValue (Ft.Interval.SECOND)); var expression_4 = Ft.Expression.parse ("-1000000") as Ft.Constant; assert_value_equals (expression_4?.value, new Ft.IntervalValue (-Ft.Interval.SECOND)); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_numeric_literal__syntax_error () { string[] invalid_texts = { "-", "123D" }; foreach (var text in invalid_texts) { try { Ft.Expression.parse (text); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.SYNTAX_ERROR); } } } /* * Identifier */ public void test_parse_identifier__variable () { try { var expression_1 = Ft.Expression.parse ("isPaused"); assert_true (expression_1 is Ft.Variable); assert_cmpstr ((expression_1 as Ft.Variable)?.name, GLib.CompareOperator.EQ, "is-paused"); var expression_2 = Ft.Expression.parse (" isPaused "); assert_true (expression_2 is Ft.Variable); assert_cmpstr ((expression_2 as Ft.Variable)?.name, GLib.CompareOperator.EQ, "is-paused"); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_identifier__boolean () { try { var true_constant = Ft.Expression.parse ("true") as Ft.Constant; assert_nonnull (true_constant); assert_true (true_constant.value is Ft.BooleanValue); var true_value = (Ft.BooleanValue) true_constant.value; assert_true (true_value.data); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } try { var false_constant = Ft.Expression.parse ("false") as Ft.Constant; assert_nonnull (false_constant); assert_true (false_constant.value is Ft.BooleanValue); var false_value = (Ft.BooleanValue) false_constant.value; assert_false (false_value.data); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_identifier__syntax_error () { string[] invalid_texts = { "isPaused isPaused", "123abc", }; foreach (var text in invalid_texts) { try { Ft.Expression.parse (text); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.SYNTAX_ERROR); } } } public void test_parse_identifier__unknown_identifier_error () { string[] unknown_names = { "abc123", "Foo", "TRUE", "True", "FALSE", "False", "null", "undefined", "NaN", }; foreach (var name in unknown_names) { try { Ft.Expression.parse (name); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.UNKNOWN_IDENTIFIER); } } } /* * Operators */ public void test_parse_operator__or () { try { var expression_1 = Ft.Expression.parse ( "isPaused || isFinished") as Ft.Operation; assert_nonnull (expression_1); assert_true (expression_1.operator == Ft.Operator.OR); assert_cmpstr (expression_1.to_string (), GLib.CompareOperator.EQ, "isPaused || isFinished"); var expression_2 = Ft.Expression.parse ( "isPaused||isFinished||isStarted") as Ft.Operation; assert_nonnull (expression_2); assert_true (expression_2.operator == Ft.Operator.OR); assert_cmpstr (expression_2.to_string (), GLib.CompareOperator.EQ, "isPaused || isFinished || isStarted"); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_operator__and () { try { var expression_1 = Ft.Expression.parse ( "isPaused && isFinished") as Ft.Operation; assert_nonnull (expression_1); assert_true (expression_1.operator == Ft.Operator.AND); assert_cmpstr (expression_1.to_string (), GLib.CompareOperator.EQ, "isPaused && isFinished"); var expression_2 = Ft.Expression.parse ( "isPaused&&isFinished&&isStarted") as Ft.Operation; assert_nonnull (expression_2); assert_true (expression_2.operator == Ft.Operator.AND); assert_cmpstr (expression_2.to_string (), GLib.CompareOperator.EQ, "isPaused && isFinished && isStarted"); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_operator__eq () { try { var expression_1 = Ft.Expression.parse ( "isPaused == isFinished") as Ft.Comparison; assert_nonnull (expression_1); assert_true (expression_1.operator == Ft.Operator.EQ); var expression_1_lhs = expression_1.argument_lhs as Ft.Variable; assert_cmpstr (expression_1_lhs.name, GLib.CompareOperator.EQ, "is-paused"); var expression_1_rhs = expression_1.argument_rhs as Ft.Variable; assert_cmpstr (expression_1_rhs.name, GLib.CompareOperator.EQ, "is-finished"); // TODO: make a chain of comparisons using AND operator // var expression_2 = Ft.Expression.parse ( // "isPaused == isFinished == isRunning") as Ft.Operation; // assert_nonnull (expression_1); // assert_true (expression_1.operator == Ft.Operator.AND); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_operator__gt () { try { var expression_1 = Ft.Expression.parse ( "isPaused > isFinished") as Ft.Comparison; assert_nonnull (expression_1); assert_true (expression_1.operator == Ft.Operator.GT); var expression_1_lhs = expression_1.argument_lhs as Ft.Variable; assert_cmpstr (expression_1_lhs.name, GLib.CompareOperator.EQ, "is-paused"); var expression_1_rhs = expression_1.argument_rhs as Ft.Variable; assert_cmpstr (expression_1_rhs.name, GLib.CompareOperator.EQ, "is-finished"); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } /* * Parentheses */ public void test_parse_parentheses () { try { var expression_1 = Ft.Expression.parse ("(isPaused)") as Ft.Variable; assert_nonnull (expression_1); assert_cmpstr (expression_1.name, GLib.CompareOperator.EQ, "is-paused"); assert_cmpstr (expression_1.to_string (), GLib.CompareOperator.EQ, "isPaused"); var expression_2 = Ft.Expression.parse ("((isPaused))") as Ft.Variable; assert_nonnull (expression_2); assert_cmpstr (expression_2.name, GLib.CompareOperator.EQ, "is-paused"); assert_cmpstr (expression_2.to_string (), GLib.CompareOperator.EQ, "isPaused"); var expression_3 = Ft.Expression.parse ( "(isPaused && isStarted)") as Ft.Operation; assert_nonnull (expression_3); assert_true (expression_3.operator == Ft.Operator.AND); assert_cmpstr (expression_3.to_string (), GLib.CompareOperator.EQ, "isPaused && isStarted"); var expression_4 = Ft.Expression.parse ( "(isPaused || isStarted) && isFinished") as Ft.Operation; assert_nonnull (expression_4); assert_true (expression_4.operator == Ft.Operator.AND); assert_cmpstr (expression_4.to_string (), GLib.CompareOperator.EQ, "(isPaused || isStarted) && isFinished"); var expression_5 = Ft.Expression.parse ("()"); assert_null (expression_5); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_parse_parentheses__syntax_error () { string[] invalid_texts = { "(", ")", "())", "(()", }; foreach (var text in invalid_texts) { try { Ft.Expression.parse (text); GLib.error ("No error raised for '%s'", text); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.SYNTAX_ERROR); } } } /* * Precedence */ public void test_precedence__logical_operator () { try { /* * || * / \ * false && * / \ * false true */ var expression_1 = Ft.Expression.parse ("false || false && true"); assert_cmpstr (expression_1.to_string (), GLib.CompareOperator.EQ, "false || false && true"); assert_root_has_operator (expression_1, Ft.Operator.OR); /* * || * / \ * && true * / \ * false true */ var expression_2 = Ft.Expression.parse ("false && false || true"); assert_cmpstr (expression_2.to_string (), GLib.CompareOperator.EQ, "false && false || true"); assert_root_has_operator (expression_2, Ft.Operator.OR); var expression_3 = Ft.Expression.parse ( "false && false && false || true || true"); assert_cmpstr (expression_3.to_string (), GLib.CompareOperator.EQ, "false && false && false || true || true"); assert_root_has_operator (expression_3, Ft.Operator.OR); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } public void test_precedence__comparison_operator () { try { var expression_1 = Ft.Expression.parse ("true && false != true"); assert_cmpstr (expression_1.to_string (), GLib.CompareOperator.EQ, "true && false != true"); assert_root_has_operator (expression_1, Ft.Operator.AND); var expression_2 = Ft.Expression.parse ("true != false && true"); assert_cmpstr (expression_2.to_string (), GLib.CompareOperator.EQ, "true != false && true"); assert_root_has_operator (expression_2, Ft.Operator.AND); var expression_3 = Ft.Expression.parse ("true || false != true"); assert_cmpstr (expression_3.to_string (), GLib.CompareOperator.EQ, "true || false != true"); assert_root_has_operator (expression_3, Ft.Operator.OR); } catch (Ft.ExpressionParserError error) { assert_no_error (error); } } /* * Misc */ public void test_syntax_error () { string[] invalid_texts = { "/* comment */", "// comment", ";", "arr[0]", "[1, 2, 3]", "**", "(&& isStarted)", "^", "isPaused ==", }; foreach (var text in invalid_texts) { try { Ft.Expression.parse (text); } catch (Ft.ExpressionParserError error) { assert_error (error, this.error_domain, Ft.ExpressionParserError.SYNTAX_ERROR); } } } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.ExpressionParserTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-expression.vala000066400000000000000000000551171520625676500240530ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public inline void assert_value_equals (Ft.Value value, Ft.Value expected_value) { assert_cmpstr (value.get_type_name (), GLib.CompareOperator.EQ, expected_value.get_type_name ()); assert_cmpvariant (value.to_variant (), expected_value.to_variant ()); } public class ConstantTest : Tests.TestSuite { private Ft.Context? context; public ConstantTest () { this.add_test ("state", this.test_state); this.add_test ("timestamp", this.test_timestamp); this.add_test ("interval", this.test_interval); this.add_test ("boolean", this.test_boolean); } public override void setup () { this.context = new Ft.Context (); } public override void teardown () { this.context = null; } public void test_state () { var state = Ft.State.BREAK; var constant = new Ft.Constant (new Ft.StateValue (state)); try { assert_value_equals (constant.evaluate (this.context), new Ft.StateValue (state)); } catch (Ft.ExpressionError error) { assert_no_error (error); } assert_cmpstr (constant.to_string (), GLib.CompareOperator.EQ, @"\"$(state.to_string())\""); } public void test_timestamp () { var timestamp = Ft.Timestamp.from_now (); var constant = new Ft.Constant (new Ft.TimestampValue (timestamp)); try { assert_value_equals (constant.evaluate (this.context), new Ft.TimestampValue (timestamp)); } catch (Ft.ExpressionError error) { assert_no_error (error); } assert_cmpstr (constant.to_string (), GLib.CompareOperator.EQ, @"\"$(Ft.Timestamp.to_iso8601(timestamp))\""); } public void test_interval () { var interval = Ft.Interval.HOUR; var constant = new Ft.Constant (new Ft.IntervalValue (interval)); try { assert_value_equals ( constant.evaluate (this.context), new Ft.IntervalValue (interval) ); } catch (Ft.ExpressionError error) { assert_no_error (error); } assert_cmpstr (constant.to_string (), GLib.CompareOperator.EQ, interval.to_string ()); } public void test_boolean () { var constant = new Ft.Constant (new Ft.BooleanValue (true)); try { assert_value_equals (constant.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } assert_cmpstr (constant.to_string (), GLib.CompareOperator.EQ, "true"); } } public class VariableTest : Tests.TestSuite { private Ft.Context? context; public VariableTest () { this.add_test ("timestamp", this.test_timestamp); this.add_test ("state", this.test_state); this.add_test ("status", this.test_status); this.add_test ("is_started", this.test_is_started); this.add_test ("is_paused", this.test_is_paused); this.add_test ("is_finished", this.test_is_finished); this.add_test ("is_running", this.test_is_running); this.add_test ("duration", this.test_duration); this.add_test ("offset", this.test_offset); this.add_test ("elapsed", this.test_elapsed); this.add_test ("remaining", this.test_remaining); this.add_test ("to_string", this.test_to_string); } public override void setup () { var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_time_range (1000, 1600); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var context = new Ft.Context (); context.timestamp = 1000; context.timer_state = Ft.TimerState () { duration = 600, user_data = time_block }; context.time_block = time_block; this.context = context; } public override void teardown () { this.context = null; } public void test_timestamp () { var variable = new Ft.Variable ("timestamp"); try { assert_value_equals (variable.evaluate (this.context), new Ft.TimestampValue (1000)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_state () { var variable = new Ft.Variable ("state"); try { assert_value_equals (variable.evaluate (this.context), new Ft.StateValue (Ft.State.SHORT_BREAK)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_status () { var variable = new Ft.Variable ("status"); try { assert_value_equals (variable.evaluate (this.context), new Ft.StatusValue (Ft.TimeBlockStatus.COMPLETED)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_is_started () { var variable = new Ft.Variable ("is-started"); try { assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_is_paused () { var variable = new Ft.Variable ("is-paused"); try { this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); this.context.timer_state.paused_time = 1400; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_is_finished () { var variable = new Ft.Variable ("is-finished"); try { this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); this.context.timer_state.finished_time = 1600; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_is_running () { var variable = new Ft.Variable ("is-running"); try { assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (true)); this.context.timer_state.paused_time = 1400; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); this.context.timer_state.paused_time = Ft.Timestamp.UNDEFINED; this.context.timer_state.finished_time = 1600; assert_value_equals (variable.evaluate (this.context), new Ft.BooleanValue (false)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_duration () { var variable = new Ft.Variable ("duration"); try { assert_value_equals (variable.evaluate (this.context), new Ft.IntervalValue (600)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_offset () { var variable = new Ft.Variable ("offset"); try { assert_value_equals (variable.evaluate (this.context), new Ft.IntervalValue (0)); this.context.timer_state.offset = 60; assert_value_equals (variable.evaluate (this.context), new Ft.IntervalValue (60)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_elapsed () { var variable = new Ft.Variable ("elapsed"); try { this.context.timestamp = 1300; this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.IntervalValue (100)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_remaining () { var variable = new Ft.Variable ("remaining"); try { this.context.timestamp = 1300; this.context.timer_state.started_time = 1200; assert_value_equals (variable.evaluate (this.context), new Ft.IntervalValue (500)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_to_string () { var variable = new Ft.Variable ("is-paused"); assert_cmpstr (variable.to_string (), GLib.CompareOperator.EQ, "isPaused"); } } public class OperationTest : Tests.TestSuite { private Ft.Context? context; public OperationTest () { this.add_test ("and__boolean", this.test_and__boolean); this.add_test ("and__timestamp", this.test_and__timestamp); this.add_test ("and__state", this.test_and__state); this.add_test ("to_string__single_argument", this.test_to_string__single_argument); this.add_test ("to_string__nested", this.test_to_string__nested); this.add_test ("to_string__wrap_argument", this.test_to_string__wrap_argument); } public override void setup () { this.context = new Ft.Context (); } public override void teardown () { this.context = null; } public void test_and__boolean () { bool[,] cases = { { false, false, false }, { false, true, false }, { true, false, false }, { true, true, true } }; for (var index=0; index < 4; index++) { var value_1 = new Ft.BooleanValue (cases[index, 0]); var value_2 = new Ft.BooleanValue (cases[index, 1]); var expected_result = new Ft.BooleanValue (cases[index, 2]); var operation = new Ft.Operation (Ft.Operator.AND, new Ft.Constant (value_1), new Ft.Constant (value_2)); try { assert_value_equals (operation.evaluate (this.context), expected_result); } catch (Ft.ExpressionError error) { assert_no_error (error); } } } public void test_and__timestamp () { var value_1 = new Ft.TimestampValue (Ft.Timestamp.UNDEFINED); var value_2 = new Ft.TimestampValue (Ft.Timestamp.from_now ()); var value_3 = new Ft.BooleanValue (true); try { var operation_1 = new Ft.Operation (Ft.Operator.AND, new Ft.Constant (value_1), new Ft.Constant (value_3)); assert_value_equals (operation_1.evaluate (this.context), new Ft.BooleanValue (false)); var operation_2 = new Ft.Operation (Ft.Operator.AND, new Ft.Constant (value_2), new Ft.Constant (value_3)); assert_value_equals (operation_2.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_and__state () { var value_1 = new Ft.StateValue (Ft.State.STOPPED); var value_2 = new Ft.StateValue (Ft.State.POMODORO); var value_3 = new Ft.BooleanValue (true); try { var operation_1 = new Ft.Operation (Ft.Operator.AND, new Ft.Constant (value_1), new Ft.Constant (value_3)); assert_value_equals (operation_1.evaluate (this.context), new Ft.BooleanValue (false)); var operation_2 = new Ft.Operation (Ft.Operator.AND, new Ft.Constant (value_2), new Ft.Constant (value_3)); assert_value_equals (operation_2.evaluate (this.context), new Ft.BooleanValue (true)); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_to_string__single_argument () { var argument_1 = new Ft.Variable ("is-paused"); var argument_2 = new Ft.Constant ( new Ft.IntervalValue (Ft.Interval.HOUR)); var argument_3 = new Ft.Comparison ( new Ft.Variable ("state"), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO))); var argument_4 = new Ft.Operation (Ft.Operator.OR, argument_1, argument_3); var operation_1 = new Ft.Operation (Ft.Operator.AND, argument_1); assert_cmpstr (operation_1.to_string (), GLib.CompareOperator.EQ, argument_1.to_string ()); var operation_2 = new Ft.Operation (Ft.Operator.AND, argument_2); assert_cmpstr (operation_2.to_string (), GLib.CompareOperator.EQ, argument_2.to_string ()); var operation_3 = new Ft.Operation (Ft.Operator.AND, argument_3); assert_cmpstr (operation_3.to_string (), GLib.CompareOperator.EQ, argument_3.to_string ()); var operation_4 = new Ft.Operation (Ft.Operator.AND, argument_4); assert_cmpstr (operation_4.to_string (), GLib.CompareOperator.EQ, argument_4.to_string ()); } public void test_to_string__nested () { var argument_1 = new Ft.Variable ("is-started"); var argument_2 = new Ft.Variable ("is-paused"); var argument_3 = new Ft.Variable ("is-running"); var argument_4 = new Ft.Variable ("is-finished"); var operation_1 = new Ft.Operation ( Ft.Operator.AND, new Ft.Operation (Ft.Operator.OR, argument_1, argument_2), new Ft.Operation (Ft.Operator.OR, argument_3, argument_4) ); assert_cmpstr (operation_1.to_string (), GLib.CompareOperator.EQ, "(isStarted || isPaused) && (isRunning || isFinished)"); var operation_2 = new Ft.Operation ( Ft.Operator.OR, new Ft.Operation (Ft.Operator.AND, argument_1, argument_2), new Ft.Operation (Ft.Operator.AND, argument_3, argument_4) ); assert_cmpstr (operation_2.to_string (), GLib.CompareOperator.EQ, "isStarted && isPaused || isRunning && isFinished"); } public void test_to_string__wrap_argument () { var argument_1 = new Ft.Variable ("state"); var argument_2 = new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO)); var argument_3 = new Ft.Variable ("is-started"); var argument_4 = new Ft.Variable ("is-paused"); var operation = new Ft.Operation ( Ft.Operator.AND, new Ft.Comparison (argument_1, Ft.Operator.EQ, argument_2), new Ft.Operation (Ft.Operator.OR, argument_3, argument_4) ); assert_cmpstr (operation.to_string (), GLib.CompareOperator.EQ, "state == \"pomodoro\" && (isStarted || isPaused)"); } } public class ComparisonTest : Tests.TestSuite { private Ft.Context? context; public ComparisonTest () { this.add_test ("eq__state", this.test_eq__state); this.add_test ("not_eq__state", this.test_not_eq__state); this.add_test ("to_string__simple", this.test_to_string__simple); this.add_test ("to_string__nested", this.test_to_string__nested); this.add_test ("to_string__is_true", this.test_to_string__is_true); } public override void setup () { this.context = new Ft.Context (); } public override void teardown () { this.context = null; } public void test_eq__state () { var comparison_1 = new Ft.Comparison ( new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO)), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO))); try { assert_true (comparison_1.evaluate (this.context)?.to_boolean ()); } catch (Ft.ExpressionError error) { assert_no_error (error); } var comparison_2 = new Ft.Comparison ( new Ft.Constant (new Ft.StateValue (Ft.State.SHORT_BREAK)), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.BREAK))); try { assert_true (comparison_2.evaluate (this.context)?.to_boolean ()); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_not_eq__state () { var comparison = new Ft.Comparison ( new Ft.Constant (new Ft.StateValue (Ft.State.STOPPED)), Ft.Operator.NOT_EQ, new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO))); try { assert_true (comparison.evaluate (this.context)?.to_boolean ()); } catch (Ft.ExpressionError error) { assert_no_error (error); } } public void test_to_string__simple () { var comparison = new Ft.Comparison ( new Ft.Variable ("state"), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO))); assert_cmpstr (comparison.to_string (), GLib.CompareOperator.EQ, "state == \"pomodoro\""); } public void test_to_string__nested () { var comparison_1 = new Ft.Comparison ( new Ft.Variable ("state"), Ft.Operator.EQ, new Ft.Constant (new Ft.StateValue (Ft.State.POMODORO))); var comparison_2 = new Ft.Comparison ( new Ft.Variable ("duration"), Ft.Operator.GT, new Ft.Constant (new Ft.IntervalValue (Ft.Interval.MINUTE))); var comparison = new Ft.Comparison (comparison_1, Ft.Operator.EQ, comparison_2); assert_cmpstr (comparison.to_string (), GLib.CompareOperator.EQ, "(state == \"pomodoro\") == (duration > 60000000)"); } public void test_to_string__is_true () { var comparison = new Ft.Comparison.is_true (new Ft.Variable ("is-started")); assert_cmpstr (comparison.to_string (), GLib.CompareOperator.EQ, "isStarted"); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.ConstantTest (), new Tests.VariableTest (), new Tests.OperationTest (), new Tests.ComparisonTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-job-queue.vala000066400000000000000000000102651520625676500235430ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private class DummyJob : GLib.Object, Ft.Job { public uint delay { get; construct; } public bool completed { get; set; default = false; } public GLib.Error? error { get; set; default = null; } public DummyJob (uint delay) { Object (delay: delay); } public async bool run () throws GLib.Error { GLib.Timeout.add (this.delay, () => { this.completed = true; run.callback (); return GLib.Source.REMOVE; }); yield; return true; } } public class JobQueueTest : Tests.MainLoopTestSuite { public JobQueueTest () { this.add_test ("push__simple", this.test_push__simple); this.add_test ("push__many", this.test_push__many); this.add_test ("wait__empty", this.test_wait__empty); this.add_test ("wait", this.test_wait); } public void test_push__simple () { var queue = new Ft.JobQueue (); var job = new DummyJob (10); job.notify["completed"].connect (() => { this.quit_main_loop (); }); queue.push (job); assert_true (this.run_main_loop (1000)); assert_true (job.completed); } public void test_push__many () { var queue = new Ft.JobQueue (); var job_1 = new DummyJob (10); var job_2 = new DummyJob (10); var job_3 = new DummyJob (10); var completed_jobs = new string[0]; job_1.notify["completed"].connect (() => { completed_jobs += "job-1"; if (completed_jobs.length == 3) { this.quit_main_loop (); } }); job_2.notify["completed"].connect (() => { completed_jobs += "job-2"; if (completed_jobs.length == 3) { this.quit_main_loop (); } }); job_3.notify["completed"].connect (() => { completed_jobs += "job-3"; if (completed_jobs.length == 3) { this.quit_main_loop (); } }); queue.push (job_1); queue.push (job_2); queue.push (job_3); assert_true (this.run_main_loop (1000)); assert_true (job_1.completed); assert_true (job_2.completed); assert_true (job_3.completed); assert_cmpint (completed_jobs.length, GLib.CompareOperator.EQ, 3); assert_cmpstrv (completed_jobs, { "job-1", "job-2", "job-3" }); } public void test_wait__empty () { var queue = new Ft.JobQueue (); var returned = false; queue.wait.begin ((obj, res) => { queue.wait.end (res); returned = true; this.quit_main_loop (); }); assert_true (this.run_main_loop (1000)); assert_true (returned); } public void test_wait () { var queue = new Ft.JobQueue (); var job_1 = new DummyJob (10); var job_2 = new DummyJob (10); var job_3 = new DummyJob (10); queue.push (job_1); queue.push (job_2); queue.push (job_3); assert_false (job_1.completed); var returned = false; queue.wait.begin ((obj, res) => { queue.wait.end (res); returned = true; this.quit_main_loop (); }); assert_true (this.run_main_loop (1000)); assert_true (returned); assert_true (job_1.completed); assert_true (job_2.completed); assert_true (job_3.completed); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.JobQueueTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-matrix.vala000066400000000000000000000322671520625676500231610ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class MatrixTest : Tests.TestSuite { public MatrixTest () { this.add_test ("get", this.test_get); this.add_test ("set", this.test_set); this.add_test ("resize", this.test_resize); this.add_test ("resize__default_value", this.test_resize__default_value); this.add_test ("add", this.test_add); this.add_test ("min", this.test_min); this.add_test ("max", this.test_max); this.add_test ("sum", this.test_sum); this.add_test ("unstack", this.test_unstack); } public void test_get () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); assert_cmpfloat (matrix.@get (0, 0), GLib.CompareOperator.EQ, 4.0); assert_cmpfloat (matrix.@get (1, 1), GLib.CompareOperator.EQ, 7.0); assert_cmpfloat (matrix.@get (0, 1), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat (matrix.@get (1, 0), GLib.CompareOperator.EQ, 2.0); assert_cmpfloat (matrix.@get (1, 2), GLib.CompareOperator.EQ, 8.0); assert_true (matrix.@get (2, 0).is_nan ()); assert_true (matrix.@get (0, 3).is_nan ()); } public void test_set () { var matrix = new Ft.Matrix (2, 3); matrix.@set (0, 0, 4.0); assert_cmpfloat (matrix.@get (0, 0), GLib.CompareOperator.EQ, 4.0); matrix.@set (1, 1, 7.0); assert_cmpfloat (matrix.@get (1, 1), GLib.CompareOperator.EQ, 7.0); matrix.@set (1, 2, 8.0); assert_cmpfloat (matrix.@get (1, 2), GLib.CompareOperator.EQ, 8.0); matrix.@set (2, 0, 100.0); assert_true (matrix.@get (2, 0).is_nan ()); matrix.@set (0, 3, 100.0); assert_true (matrix.@get (0, 3).is_nan ()); } public void test_resize () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); var expected_result = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0, 0.0, 0.0 }, { 2.0, 7.0, 8.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0, 0.0, 0.0 } }); matrix.resize (expected_result.shape[0], expected_result.shape[1]); // GLib.debug ("result = %s", matrix.to_representation ()); assert_true (matrix.equals (expected_result)); expected_result = new Ft.Matrix.from_array ({ { 4.0, 1.0 } }); matrix.resize (1, 2); // GLib.debug ("result = %s", matrix.to_representation ()); assert_true (matrix.equals (expected_result)); } public void test_resize__default_value () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); matrix.resize (3, 5, 5.5); // Original values are preserved assert_cmpfloat (matrix.@get (0, 0), GLib.CompareOperator.EQ, 4.0); assert_cmpfloat (matrix.@get (1, 2), GLib.CompareOperator.EQ, 8.0); // Newly created cells are initialized with default_value (5.5) assert_cmpfloat (matrix.@get (0, 3), GLib.CompareOperator.EQ, 5.5); assert_cmpfloat (matrix.@get (0, 4), GLib.CompareOperator.EQ, 5.5); assert_cmpfloat (matrix.@get (2, 0), GLib.CompareOperator.EQ, 5.5); assert_cmpfloat (matrix.@get (2, 4), GLib.CompareOperator.EQ, 5.5); } public void test_add () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); var other = new Ft.Matrix.from_array ({ { 0.0, 2.0, -3.0 }, { 6.0, 1.0, 1.0 } }); var expected_result = new Ft.Matrix.from_array ({ { 4.0, 3.0, 0.0 }, { 8.0, 8.0, 9.0 } }); assert_true (matrix.add (other)); // GLib.debug ("result = %s", matrix.to_representation ()); assert_true (matrix.equals (expected_result)); } public void test_min () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); assert_cmpfloat (matrix.min (), GLib.CompareOperator.EQ, 1.0); } public void test_max () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); assert_cmpfloat (matrix.max (), GLib.CompareOperator.EQ, 8.0); } public void test_sum () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); assert_cmpfloat (matrix.sum (), GLib.CompareOperator.EQ, 25.0); } public void test_unstack () { var matrix = new Ft.Matrix.from_array ({ { 4.0, 1.0, 3.0 }, { 2.0, 7.0, 8.0 } }); var result_0 = matrix.unstack (0); assert_cmpuint (result_0.length, GLib.CompareOperator.EQ, matrix.shape[0]) ; assert_true (result_0[0].equals (matrix.get_vector (0, 0))); assert_true (result_0[1].equals (matrix.get_vector (0, 1))); var result_1 = matrix.unstack (1); assert_cmpuint (result_1.length, GLib.CompareOperator.EQ, matrix.shape[1]) ; assert_true (result_1[0].equals (matrix.get_vector (1, 0))); assert_true (result_1[1].equals (matrix.get_vector (1, 1))); assert_true (result_1[2].equals (matrix.get_vector (1, 2))); } } public class Matrix3DTest : Tests.TestSuite { public Matrix3DTest () { this.add_test ("get", this.test_get); this.add_test ("set", this.test_set); this.add_test ("resize", this.test_resize); this.add_test ("resize__default_value", this.test_resize__default_value); this.add_test ("min", this.test_min); this.add_test ("max", this.test_max); this.add_test ("sum", this.test_sum); this.add_test ("unstack", this.test_unstack); } public void test_get () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); assert_cmpfloat (matrix.@get (0, 0, 1), GLib.CompareOperator.EQ, -1.0); assert_cmpfloat (matrix.@get (1, 0, 0), GLib.CompareOperator.EQ, 2.0); assert_cmpfloat (matrix.@get (0, 1, 0), GLib.CompareOperator.EQ, 1.0); assert_true (matrix.@get (2, 0, 0).is_nan ()); assert_true (matrix.@get (0, 3, 0).is_nan ()); assert_true (matrix.@get (0, 0, 2).is_nan ()); } public void test_set () { var matrix = new Ft.Matrix3D (2, 3, 2); matrix.@set (0, 0, 1, 4.0); assert_cmpfloat (matrix.@get (0, 0, 1), GLib.CompareOperator.EQ, 4.0); matrix.@set (0, 1, 0, 7.0); assert_cmpfloat (matrix.@get (0, 1, 0), GLib.CompareOperator.EQ, 7.0); matrix.@set (1, 2, 1, 8.0); assert_cmpfloat (matrix.@get (1, 2, 1), GLib.CompareOperator.EQ, 8.0); matrix.@set (2, 0, 0, 100.0); assert_true (matrix.@get (2, 0, 0).is_nan ()); matrix.@set (0, 3, 0, 100.0); assert_true (matrix.@get (0, 3, 0).is_nan ()); matrix.@set (0, 0, 2, 100.0); assert_true (matrix.@get (0, 0, 2).is_nan ()); } public void test_resize () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); var expected_result = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0, 0.0, 0.0 }, { 1.0, 0.0, 0.0, 0.0 }, { 3.0, 9.0, 0.0, 0.0 } }, { { 2.0, 3.0, 0.0, 0.0 }, { 7.0, 1.0, 0.0, 0.0 }, { 8.0, 0.0, 0.0, 0.0 } }, { { 0.0, 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0, 0.0 }, { 0.0, 0.0, 0.0, 0.0 } } }); matrix.resize (expected_result.shape[0], expected_result.shape[1], expected_result.shape[2]); assert_true (matrix.equals (expected_result)); } public void test_resize__default_value () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); matrix.resize (3, 3, 4, 2.5); // Preserved original values assert_cmpfloat (matrix.@get (0, 0, 0), GLib.CompareOperator.EQ, 4.0); assert_cmpfloat (matrix.@get (1, 2, 0), GLib.CompareOperator.EQ, 8.0); // Newly created cells are initialized with default_value (2.5) assert_cmpfloat (matrix.@get (0, 0, 2), GLib.CompareOperator.EQ, 2.5); assert_cmpfloat (matrix.@get (0, 2, 3), GLib.CompareOperator.EQ, 2.5); assert_cmpfloat (matrix.@get (2, 0, 0), GLib.CompareOperator.EQ, 2.5); assert_cmpfloat (matrix.@get (2, 2, 3), GLib.CompareOperator.EQ, 2.5); } public void test_min () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); assert_cmpfloat (matrix.min (), GLib.CompareOperator.EQ, -1.0); } public void test_max () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); assert_cmpfloat (matrix.max (), GLib.CompareOperator.EQ, 9.0); } public void test_sum () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); assert_cmpfloat (matrix.sum (), GLib.CompareOperator.EQ, 37.0); } public void test_unstack () { var matrix = new Ft.Matrix3D.from_array ({ { { 4.0, -1.0 }, { 1.0, 0.0 }, { 3.0, 9.0 } }, { { 2.0, 3.0 }, { 7.0, 1.0 }, { 8.0, 0.0 } } }); var result_0 = matrix.unstack (0); assert_cmpuint (result_0.length, GLib.CompareOperator.EQ, matrix.shape[0]); assert_true (result_0[0].equals (matrix.get_matrix (0, 0))); assert_true (result_0[1].equals (matrix.get_matrix (0, 1))); var result_1 = matrix.unstack (1); assert_cmpuint (result_1.length, GLib.CompareOperator.EQ, matrix.shape[1]); assert_true (result_1[0].equals (matrix.get_matrix (1, 0))); assert_true (result_1[1].equals (matrix.get_matrix (1, 1))); assert_true (result_1[2].equals (matrix.get_matrix (1, 2))); var result_2 = matrix.unstack (2); assert_cmpuint (result_2.length, GLib.CompareOperator.EQ, matrix.shape[2]); assert_true (result_2[0].equals (matrix.get_matrix (2, 0))); assert_true (result_2[1].equals (matrix.get_matrix (2, 1))); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.MatrixTest (), new Tests.Matrix3DTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-notification-manager.vala000066400000000000000000000255201520625676500257450ustar00rootroot00000000000000namespace Tests { private class MockNotificationBackend : GLib.Object, Ft.NotificationBackendInterface { public string[] log; construct { this.log = {}; } public void withdraw_notification (string id) { this.log += @"withdraw:$(id)"; } public void send_notification (string id, Ft.Notification notification) { var entry = @"send:$(id):$(notification.title):$(notification.body)"; this.log += entry; this.logged (entry); } public void clear () { this.log = {}; } public signal void logged (string entry); public override void dispose () { this.log = null; base.dispose (); } } public class NotificationManagerTest : Tests.MainLoopTestSuite { private Ft.Timer timer; private Ft.SessionManager session_manager; private Ft.NotificationManager notification_manager; public NotificationManagerTest () { this.add_test ("time_block_started", this.test_time_block_started); this.add_test ("time_block_running", this.test_time_block_running); this.add_test ("time_block_about_to_end", this.test_time_block_about_to_end); this.add_test ("time_block_ended", this.test_time_block_ended); this.add_test ("confirm_advancement__break", this.test_confirm_advancement__break); this.add_test ("confirm_advancement__pomodoro", this.test_confirm_advancement__pomodoro); this.add_test ("withdraw_notifications__pause", this.test_withdraw_notifications__pause); this.add_test ("withdraw_notifications__stop", this.test_withdraw_notifications__stop); this.add_test ("request_screen_overlay", this.test_request_screen_overlay); } public override void setup () { base.setup (); Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); this.session_manager = new Ft.SessionManager.with_timer (this.timer); Ft.SessionManager.set_default (this.session_manager); this.notification_manager = new Ft.NotificationManager.with_backend ( new MockNotificationBackend ()); assert (!this.notification_manager.get_data ("teardown")); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("announce-about-to-end", false); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); settings.set_boolean ("screen-overlay", false); this.session_manager.ensure_session (); } public override void teardown () { this.notification_manager.set_data ("teardown", true); this.notification_manager.destroy (); this.notification_manager = null; this.session_manager = null; this.timer = null; Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); base.teardown (); } private MockNotificationBackend get_mock_backend () { return this.notification_manager.backend as MockNotificationBackend; } public void test_time_block_started () { var backend = this.get_mock_backend (); backend.clear (); this.timer.start (); assert_cmpstrv (backend.log, { "send:timer:Pomodoro:25 minutes remaining" }); } public void test_time_block_running () { this.timer.start (); // Pause and resume to trigger notify_time_block_running Ft.Timestamp.advance (Ft.Interval.MINUTE); var backend = this.get_mock_backend (); backend.clear (); this.timer.pause (); assert_cmpstrv (backend.log, { "withdraw:timer" }); backend.clear (); this.timer.resume (); assert_cmpstrv (backend.log, { "send:timer:Pomodoro:24 minutes remaining" }); } public void test_time_block_about_to_end () { var settings = Ft.get_settings (); settings.set_boolean ("announce-about-to-end", true); this.timer.start (); var backend = this.get_mock_backend (); backend.clear (); var time_block = this.session_manager.current_time_block; // Advance time to just BEFORE about-to-end threshold var timestamp_1 = time_block.end_time - 16 * Ft.Interval.SECOND; Ft.Timestamp.freeze_to (timestamp_1); this.timer.tick (timestamp_1); assert_cmpint (backend.log.length, GLib.CompareOperator.EQ, 0); // Advance time into the threshold var timestamp_2 = time_block.end_time - 10 * Ft.Interval.SECOND; Ft.Timestamp.freeze_to (timestamp_2); this.timer.tick (timestamp_2); assert_cmpstrv (backend.log, { @"send:timer:Pomodoro is about to end:" }); // Another tick should NOT trigger another notification backend.clear (); var timestamp_3 = time_block.end_time - 9 * Ft.Interval.SECOND; Ft.Timestamp.freeze_to (timestamp_3); this.timer.tick (timestamp_3); assert_cmpstrv (backend.log, {}); } public void test_time_block_ended () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-pomodoro", false); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); var backend = this.get_mock_backend (); backend.clear (); var now = this.session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (now); this.timer.finish (now); assert_cmpstrv (backend.log, { "send:timer:Break is over!:Get ready…" }); } public void test_confirm_advancement__break () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); this.session_manager.advance_to_state (Ft.State.POMODORO); var backend = this.get_mock_backend (); backend.clear (); var confirm_advancement_emitted = 0; this.session_manager.confirm_advancement.connect ( (current_time_block, next_time_block) => { confirm_advancement_emitted++; }); var now = this.session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (now); this.timer.finish (now); assert_cmpint ( confirm_advancement_emitted, GLib.CompareOperator.EQ, 1 ); assert_cmpstrv (backend.log, { "send:timer:Pomodoro is over!:Confirm the start of a short break…" }); } public void test_confirm_advancement__pomodoro () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-pomodoro", true); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); var backend = this.get_mock_backend (); backend.clear (); var confirm_advancement_emitted = 0; this.session_manager.confirm_advancement.connect ( (current_time_block, next_time_block) => { confirm_advancement_emitted++; }); var now = this.session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (now); this.timer.finish (now); assert_cmpint ( confirm_advancement_emitted, GLib.CompareOperator.EQ, 1 ); assert_cmpstrv (backend.log, { "send:timer:Break is over!:Confirm the start of a Pomodoro…" }); } public void test_withdraw_notifications__pause () { this.session_manager.advance_to_state (Ft.State.POMODORO); var backend = this.get_mock_backend (); backend.clear (); this.timer.pause (); assert_cmpstrv (backend.log, { "withdraw:timer" }); } public void test_withdraw_notifications__stop () { this.session_manager.advance_to_state (Ft.State.POMODORO); var backend = this.get_mock_backend (); backend.clear (); this.timer.reset (); assert_cmpstrv (backend.log, { "withdraw:timer" }); } public void test_request_screen_overlay () { var settings = Ft.get_settings (); settings.set_boolean ("screen-overlay", true); var backend = this.get_mock_backend (); var open_requested = false; this.notification_manager.request_screen_overlay_open.connect (() => { open_requested = true; }); this.session_manager.advance_to_state (Ft.State.POMODORO); backend.clear (); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); assert_true (open_requested); assert_cmpstrv (backend.log, {}); // Simulate overlay opened this.notification_manager.emit_screen_overlay_opened (); assert_cmpstrv (backend.log, { "withdraw:timer" }); // Simulate overlay closed // Expect notification to be sent after a delay Ft.Timestamp.advance (Ft.Interval.MINUTE); backend.clear (); backend.logged.connect ((entry) => { this.quit_main_loop (); }); this.notification_manager.emit_screen_overlay_closed (); assert_cmpstrv (backend.log, {}); assert_true (this.run_main_loop (500)); assert_cmpstrv (backend.log, { @"send:timer:Short Break:4 minutes remaining" }); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.NotificationManagerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-provided-object.vala000066400000000000000000000276361520625676500247410ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public enum Scenario { AVAILABLE, UNAVAILABLE, DELAYED_AVAILABLE, DELAYED_UNAVAILABLE, ASYNC_AVAILABLE, ASYNC_UNAVAILABLE, NO_AVAILABILITY_REPORTED, } public interface AntiGravityProvider : Ft.Provider { public abstract string name { get; construct set; } public abstract Scenario scenario { get; construct set; } } public class SimpleAntiGravityProvider : Ft.Provider, AntiGravityProvider { public string name { get; construct set; default = ""; } public Scenario scenario { get; construct set; default = Scenario.AVAILABLE; } public uint initialize_count = 0; public uint uninitialize_count = 0; public uint enable_count = 0; public uint disable_count = 0; public SimpleAntiGravityProvider (string name, Scenario scenario = Scenario.AVAILABLE) { GLib.Object ( name: name, scenario: scenario ); } private void initialize__available () { this.available = true; } private void initialize__unavailable () { this.available = false; } private void initialize__delayed_available () { GLib.Idle.add (() => { this.available = true; return GLib.Source.REMOVE; }); } private void initialize__delayed_unavailable () { GLib.Idle.add (() => { this.available = false; return GLib.Source.REMOVE; }); } private async void initialize__async_available () { GLib.Idle.add (() => { this.initialize__async_available.callback (); return GLib.Source.REMOVE; }); yield; this.available = true; } private async void initialize__async_unavailable () { GLib.Idle.add (() => { this.initialize__async_unavailable.callback (); return GLib.Source.REMOVE; }); yield; this.available = false; } private void initialize__no_availability_reported () { } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.initialize_count++; switch (this.scenario) { case Scenario.AVAILABLE: this.initialize__available (); break; case Scenario.UNAVAILABLE: this.initialize__unavailable (); break; case Scenario.DELAYED_AVAILABLE: this.initialize__delayed_available (); break; case Scenario.DELAYED_UNAVAILABLE: this.initialize__delayed_unavailable (); break; case Scenario.NO_AVAILABILITY_REPORTED: this.initialize__no_availability_reported (); break; case Scenario.ASYNC_AVAILABLE: yield this.initialize__async_available (); break; case Scenario.ASYNC_UNAVAILABLE: yield this.initialize__async_unavailable (); break; default: assert_not_reached (); } } public override async void uninitialize () throws GLib.Error { this.uninitialize_count++; } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.enable_count++; } public override async void disable () throws GLib.Error { this.disable_count++; } public override void dispose () { base.dispose (); } } public class AntiGravity : Ft.ProvidedObject { public uint enabled_count = 0; public uint disabled_count = 0; public AntiGravity () { } public void add_provider (string name, Scenario scenario, Ft.Priority priority = Ft.Priority.DEFAULT) { this.providers.add (new SimpleAntiGravityProvider (name, scenario), priority); } protected override void initialize () { } protected override void setup_providers () { } protected override void provider_enabled (AntiGravityProvider provider) { this.enabled_count++; } protected override void provider_disabled (AntiGravityProvider provider) { this.disabled_count++; } } public class ProvidedObjectTest : Tests.TestSuite { private GLib.MainLoop? main_loop = null; private uint timeout_id = 0; public ProvidedObjectTest () { this.add_test ("available", this.test_available); this.add_test ("unavailable_1", this.test_unavailable_1); this.add_test ("unavailable_2", this.test_unavailable_2); this.add_test ("delayed_unavailable_1", this.test_delayed_unavailable_1); this.add_test ("delayed_unavailable_2", this.test_delayed_unavailable_2); this.add_test ("no_availability_reported", this.test_no_availability_reported); } public override void setup () { this.main_loop = new GLib.MainLoop (); } public override void teardown () { this.main_loop = null; } private bool run_main_loop (uint timeout = 1000) { var success = true; if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } private void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } public void test_available () { var anti_gravity = new AntiGravity (); anti_gravity.notify["enabled"].connect (() => { this.quit_main_loop (); }); anti_gravity.add_provider ("", Scenario.AVAILABLE); assert_true (this.run_main_loop ()); assert_true (anti_gravity.available); assert_true (anti_gravity.enabled); assert_nonnull (anti_gravity.provider); assert_true (anti_gravity.provider.available); assert_true (anti_gravity.provider.enabled); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } public void test_unavailable_1 () { var anti_gravity = new AntiGravity (); anti_gravity.add_provider ("", Scenario.UNAVAILABLE); assert_null (anti_gravity.provider); assert_false (anti_gravity.available); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } public void test_unavailable_2 () { var anti_gravity = new AntiGravity (); anti_gravity.add_provider ("high", Scenario.UNAVAILABLE, Ft.Priority.HIGH); anti_gravity.add_provider ("low", Scenario.AVAILABLE, Ft.Priority.LOW); anti_gravity.notify["enabled"].connect (() => { this.quit_main_loop (); }); assert_true (this.run_main_loop ()); assert_true (anti_gravity.available); assert_true (anti_gravity.enabled); assert_nonnull (anti_gravity.provider); assert_cmpstr (anti_gravity.provider.name, GLib.CompareOperator.EQ, "low"); assert_true (anti_gravity.provider.available); assert_true (anti_gravity.provider.enabled); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } public void test_delayed_unavailable_1 () { var anti_gravity = new AntiGravity (); anti_gravity.add_provider ("high", Scenario.DELAYED_UNAVAILABLE, Ft.Priority.HIGH); anti_gravity.add_provider ("low", Scenario.AVAILABLE, Ft.Priority.LOW); anti_gravity.notify["enabled"].connect (() => { this.quit_main_loop (); }); assert_true (this.run_main_loop ()); assert_true (anti_gravity.available); assert_true (anti_gravity.enabled); assert_nonnull (anti_gravity.provider); assert_cmpstr (anti_gravity.provider.name, GLib.CompareOperator.EQ, "low"); assert_true (anti_gravity.provider.available); assert_true (anti_gravity.provider.enabled); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } public void test_delayed_unavailable_2 () { var anti_gravity = new AntiGravity (); anti_gravity.add_provider ("low", Scenario.AVAILABLE, Ft.Priority.LOW); anti_gravity.add_provider ("default", Scenario.DELAYED_AVAILABLE, Ft.Priority.DEFAULT); anti_gravity.add_provider ("high", Scenario.UNAVAILABLE, Ft.Priority.HIGH); anti_gravity.notify["enabled"].connect (() => { this.quit_main_loop (); }); assert_true (this.run_main_loop ()); assert_true (anti_gravity.available); assert_true (anti_gravity.enabled); assert_nonnull (anti_gravity.provider); assert_cmpstr (anti_gravity.provider.name, GLib.CompareOperator.EQ, "default"); assert_true (anti_gravity.provider.available); assert_true (anti_gravity.provider.enabled); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } public void test_no_availability_reported () { var anti_gravity = new AntiGravity (); anti_gravity.add_provider ("high", Scenario.NO_AVAILABILITY_REPORTED, Ft.Priority.HIGH); anti_gravity.add_provider ("low", Scenario.AVAILABLE, Ft.Priority.LOW); anti_gravity.notify["enabled"].connect (() => { this.quit_main_loop (); }); assert_true (this.run_main_loop ()); assert_true (anti_gravity.available); assert_true (anti_gravity.enabled); assert_nonnull (anti_gravity.provider); assert_cmpstr (anti_gravity.provider.name, GLib.CompareOperator.EQ, "low"); assert_true (anti_gravity.provider.available); assert_true (anti_gravity.provider.enabled); assert_cmpuint (anti_gravity.enabled_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (anti_gravity.disabled_count, GLib.CompareOperator.EQ, 0); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.ProvidedObjectTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-provider-set.vala000066400000000000000000000614551520625676500243010ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public enum Scenario { AVAILABLE, UNAVAILABLE, DELAYED_AVAILABLE, DELAYED_UNAVAILABLE, ASYNC_AVAILABLE, ASYNC_UNAVAILABLE, NO_AVAILABILITY_REPORTED, } public interface AntiGravityProvider : Ft.Provider { public abstract Scenario scenario { get; construct set; } } public class SimpleAntiGravityProvider : Ft.Provider, AntiGravityProvider { public Scenario scenario { get; construct set; } public uint initialize_count = 0; public uint uninitialize_count = 0; public uint enable_count = 0; public uint disable_count = 0; public SimpleAntiGravityProvider (Scenario scenario) { GLib.Object ( scenario: scenario ); } private void initialize__available () { this.available = true; } private void initialize__unavailable () { this.available = false; } private void initialize__delayed_available () { GLib.Idle.add (() => { this.available = true; return GLib.Source.REMOVE; }); } private void initialize__delayed_unavailable () { GLib.Idle.add (() => { this.available = false; return GLib.Source.REMOVE; }); } private async void initialize__async_available () { GLib.Idle.add (() => { this.initialize__async_available.callback (); return GLib.Source.REMOVE; }); yield; this.available = true; } private async void initialize__async_unavailable () { GLib.Idle.add (() => { this.initialize__async_unavailable.callback (); return GLib.Source.REMOVE; }); yield; this.available = false; } private void initialize__no_availability_reported () { } public override async void initialize (GLib.Cancellable? cancellable) throws GLib.Error { this.initialize_count++; switch (this.scenario) { case Scenario.AVAILABLE: this.initialize__available (); break; case Scenario.UNAVAILABLE: this.initialize__unavailable (); break; case Scenario.DELAYED_AVAILABLE: this.initialize__delayed_available (); break; case Scenario.DELAYED_UNAVAILABLE: this.initialize__delayed_unavailable (); break; case Scenario.NO_AVAILABILITY_REPORTED: this.initialize__no_availability_reported (); break; case Scenario.ASYNC_AVAILABLE: yield this.initialize__async_available (); break; case Scenario.ASYNC_UNAVAILABLE: yield this.initialize__async_unavailable (); break; default: assert_not_reached (); } } public override async void uninitialize () throws GLib.Error { this.uninitialize_count++; } public override async void enable (GLib.Cancellable? cancellable) throws GLib.Error { this.enable_count++; } public override async void disable () throws GLib.Error { this.disable_count++; } public override void dispose () { base.dispose (); } } public class ProviderSetTest : Tests.TestSuite { private GLib.MainLoop? main_loop = null; private uint timeout_id = 0; public ProviderSetTest () { this.add_test ("enable_single__available", this.test_enable_single__available); this.add_test ("enable_single__unavailable", this.test_enable_single__unavailable); this.add_test ("enable_single__delayed_available_1", this.test_enable_single__delayed_available_1); this.add_test ("enable_single__delayed_available_2", this.test_enable_single__delayed_available_2); this.add_test ("enable_single__delayed_unavailable", this.test_enable_single__delayed_unavailable); this.add_test ("enable_single__async_available", this.test_enable_single__async_available); this.add_test ("enable_single__switch_to_higher_priority", this.test_enable_single__switch_to_higher_priority); this.add_test ("enable_single__no_availability_reported", this.test_enable_single__no_availability_reported); this.add_test ("enable_single__disable_when_all_unavailable", this.test_enable_single__disable_when_all_unavailable); this.add_test ("enable_single__priority_1", this.test_enable_single__priority_1); this.add_test ("enable_single__priority_2", this.test_enable_single__priority_2); this.add_test ("enable_single__priority_3", this.test_enable_single__priority_3); this.add_test ("destroy", this.test_destroy); } public override void setup () { this.main_loop = new GLib.MainLoop (); } public override void teardown () { this.main_loop = null; } private bool run_main_loop (uint timeout = 1000) { var success = true; if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } private void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } public void test_enable_single__available () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); var provider_high = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 0); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_high.available_set); assert_true (provider_high.available); assert_true (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_low.available_set); assert_false (provider_low.available); assert_false (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 0); } public void test_enable_single__unavailable () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); providers.enable (); assert_false (provider_low.enabled); assert_false (provider_high.enabled); assert_true (this.run_main_loop ()); assert_true (provider_high.available_set); assert_false (provider_high.available); assert_false (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 0); assert_true (provider_low.available_set); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.uninitialize_count, GLib.CompareOperator.EQ, 0); } public void test_enable_single__delayed_available_1 () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); var provider_high = new SimpleAntiGravityProvider (Scenario.DELAYED_AVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 0); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_high.available_set); assert_true (provider_high.available); assert_true (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 0); } /** * Add a provider after the fallback got enabled. * * Expect to switch to the better provider. */ public void test_enable_single__delayed_available_2 () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); SimpleAntiGravityProvider? added_provider = null; providers.provider_enabled.connect ( () => { if (added_provider == null) { added_provider = new SimpleAntiGravityProvider (Scenario.DELAYED_AVAILABLE); providers.add (added_provider, Ft.Priority.DEFAULT); } else { this.quit_main_loop (); } }); providers.add (provider_low, Ft.Priority.LOW); providers.enable (); assert_true (this.run_main_loop ()); assert_true (added_provider.available); assert_true (added_provider.enabled); assert_cmpuint (added_provider.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (added_provider.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_low.enabled); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.disable_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__delayed_unavailable () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); var provider_high = new SimpleAntiGravityProvider (Scenario.DELAYED_UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 0); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_high.available_set); assert_false (provider_high.available); assert_false (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 0); assert_true (provider_low.available_set); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__switch_to_higher_priority () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); providers.enable (); // Expect low priority provider to be enabled. assert_true (this.run_main_loop ()); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.disable_count, GLib.CompareOperator.EQ, 0); assert_true (provider_high.available_set); assert_false (provider_high.available); assert_false (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 0); // High priority provider becomes available. Expect to switch providers. provider_high.available = true; assert_true (this.run_main_loop ()); assert_true (provider_high.available); assert_true (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.disable_count, GLib.CompareOperator.EQ, 0); assert_false (provider_low.enabled); assert_cmpuint (provider_low.disable_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__no_availability_reported () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); var provider_high = new SimpleAntiGravityProvider (Scenario.NO_AVAILABILITY_REPORTED); providers.add (provider_high, Ft.Priority.HIGH); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_low.available_set); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_high.available_set); assert_false (provider_high.available); assert_false (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__disable_when_all_unavailable () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); var wait_for_provider_disabled = false; var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); providers.provider_enabled.connect (() => { if (!wait_for_provider_disabled) { this.quit_main_loop (); } }); providers.provider_disabled.connect ((provider) => { if (wait_for_provider_disabled && provider == provider_low) { this.quit_main_loop (); } }); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_low.enabled); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); wait_for_provider_disabled = true; GLib.Idle.add ( () => { provider_low.available = false; return GLib.Source.REMOVE; }); assert_true (this.run_main_loop ()); assert_false (provider_high.enabled); assert_false (provider_low.enabled); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (provider_low.disable_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__priority_1 () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); var provider_default = new SimpleAntiGravityProvider (Scenario.DELAYED_AVAILABLE); providers.add (provider_default, Ft.Priority.DEFAULT); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_default.available); assert_true (provider_default.enabled); assert_cmpuint (provider_default.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_default.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); } public void test_enable_single__priority_2 () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_true (provider_high.available_set); assert_false (provider_high.available); assert_false (provider_high.enabled); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 0); } public void test_enable_single__priority_3 () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_high = new SimpleAntiGravityProvider (Scenario.UNAVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); var provider_default = new SimpleAntiGravityProvider (Scenario.NO_AVAILABILITY_REPORTED); providers.add (provider_default, Ft.Priority.DEFAULT); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_low.available); assert_true (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_default.enabled); assert_cmpuint (provider_default.initialize_count, GLib.CompareOperator.EQ, 1); } public void test_enable_single__async_available () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); var provider_high = new SimpleAntiGravityProvider (Scenario.ASYNC_AVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 0); providers.enable (); assert_true (this.run_main_loop ()); assert_true (provider_high.available_set); assert_true (provider_high.available); assert_true (provider_high.enabled); assert_cmpuint (provider_high.initialize_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.enable_count, GLib.CompareOperator.EQ, 1); assert_false (provider_low.available_set); assert_false (provider_low.available); assert_false (provider_low.enabled); assert_cmpuint (provider_low.initialize_count, GLib.CompareOperator.EQ, 0); assert_cmpuint (provider_low.enable_count, GLib.CompareOperator.EQ, 0); } public void test_destroy () { var providers = new Ft.ProviderSet (Ft.SelectionMode.SINGLE); providers.provider_enabled.connect (() => { this.quit_main_loop (); }); var provider_low = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_low, Ft.Priority.LOW); providers.enable (); assert_true (this.run_main_loop ()); var provider_high = new SimpleAntiGravityProvider (Scenario.AVAILABLE); providers.add (provider_high, Ft.Priority.HIGH); assert_true (this.run_main_loop ()); providers = null; var context = GLib.MainContext.default (); while (context.iteration (false)) { } assert_false (provider_low.enabled); assert_cmpuint (provider_low.disable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_low.uninitialize_count, GLib.CompareOperator.EQ, 1); assert_false (provider_high.enabled); assert_cmpuint (provider_high.disable_count, GLib.CompareOperator.EQ, 1); assert_cmpuint (provider_high.uninitialize_count, GLib.CompareOperator.EQ, 1); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.ProviderSetTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-scheduler.vala000066400000000000000000002567551520625676500236450ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private double EPSILON = 0.0001; public abstract class BaseSchedulerTest : Tests.TestSuite { protected Ft.SessionTemplate session_template = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 4U }; public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); } public override void teardown () { Ft.Timestamp.thaw (); var settings = Ft.get_settings (); settings.revert (); } public Ft.Session create_session (Ft.Scheduler scheduler) { var session = new Ft.Session.from_template (scheduler.session_template); session.@foreach ( (time_block) => { time_block.set_intended_duration (time_block.duration); time_block.set_completion_time (scheduler.calculate_time_block_completion_time (time_block)); } ); return session; } } public class SimpleSchedulerTest : BaseSchedulerTest { public SimpleSchedulerTest () { this.add_test ("calculate_time_block_completion_time", this.test_calculate_time_block_completion_time); this.add_test ("calculate_time_block_completion_time__with_gaps", this.test_calculate_time_block_completion_time__with_gaps); this.add_test ("calculate_time_block_score__pomodoro", this.test_calculate_time_block_score__pomodoro); this.add_test ("calculate_time_block_score__extended_pomodoro", this.test_calculate_time_block_score__extended_pomodoro); this.add_test ("calculate_time_block_score__short_pomodoro", this.test_calculate_time_block_score__short_pomodoro); this.add_test ("calculate_time_block_score__short_break", this.test_calculate_time_block_score__short_break); this.add_test ("calculate_time_block_score__long_break", this.test_calculate_time_block_score__long_break); this.add_test ("calculate_time_block_score__uncompleted_pomodoro", this.test_calculate_time_block_score__uncompleted_pomodoro); this.add_test ("calculate_time_block_score__paused_pomodoro", this.test_calculate_time_block_score__paused_pomodoro); this.add_test ("calculate_time_block_weight__paused_pomodoro", this.test_calculate_time_block_weight__paused_pomodoro); this.add_test ("is_time_block_completed__pomodoro", this.test_is_time_block_completed__pomodoro); this.add_test ("is_time_block_completed__short_break", this.test_is_time_block_completed__short_break); this.add_test ("is_time_block_completed__long_break", this.test_is_time_block_completed__long_break); this.add_test ("resolve_context__update_state", this.test_resolve_context__update_state); this.add_test ("resolve_context__update_timestamp", this.test_resolve_context__update_timestamp); this.add_test ("resolve_context__completed_pomodoro", this.test_resolve_context__completed_pomodoro); this.add_test ("resolve_context__completed_short_break", this.test_resolve_context__completed_short_break); this.add_test ("resolve_context__completed_long_break", this.test_resolve_context__completed_long_break); this.add_test ("resolve_context__uncompleted_pomodoro", this.test_resolve_context__uncompleted_pomodoro); this.add_test ("resolve_context__uncompleted_short_break", this.test_resolve_context__uncompleted_short_break); this.add_test ("resolve_context__uncompleted_long_break", this.test_resolve_context__uncompleted_long_break); this.add_test ("resolve_context__uncompleted_last_pomodoro", this.test_resolve_context__uncompleted_last_pomodoro); this.add_test ("resolve_context__in_progress_pomodoro", this.test_resolve_context__in_progress_pomodoro); this.add_test ("resolve_context__in_progress_short_break", this.test_resolve_context__in_progress_short_break); this.add_test ("resolve_context__in_progress_long_break", this.test_resolve_context__in_progress_long_break); this.add_test ("resolve_context__paused_pomodoro", this.test_resolve_context__paused_pomodoro); this.add_test ("resolve_context__paused_short_break", this.test_resolve_context__paused_short_break); this.add_test ("resolve_context__needs_long_break", this.test_resolve_context__needs_long_break); this.add_test ("resolve_time_block__pomodoro", this.test_resolve_time_block__pomodoro); this.add_test ("resolve_time_block__short_break", this.test_resolve_time_block__short_break); this.add_test ("resolve_time_block__long_break", this.test_resolve_time_block__long_break); this.add_test ("resolve_time_block__completed_session", this.test_resolve_time_block__completed_session); this.add_test ("reschedule_session__populate", this.test_reschedule_session__populate); this.add_test ("reschedule_session__completed_session", this.test_reschedule_session__completed_session); this.add_test ("reschedule_session__uncompleted_pomodoro", this.test_reschedule_session__uncompleted_pomodoro); this.add_test ("reschedule_session__uncompleted_short_break", this.test_reschedule_session__uncompleted_short_break); this.add_test ("reschedule_session__uncompleted_last_pomodoro", this.test_reschedule_session__uncompleted_last_pomodoro); this.add_test ("reschedule_session__uncompleted_long_break", this.test_reschedule_session__uncompleted_long_break); this.add_test ("reschedule_session__uncompleted_extra_pomodoro", this.test_reschedule_session__uncompleted_extra_pomodoro); this.add_test ("reschedule_session__skip_uncompleted_long_break", this.test_reschedule_session__skip_uncompleted_long_break); this.add_test ("reschedule_session__skip_uncompleted_extra_pomodoro", this.test_reschedule_session__skip_uncompleted_extra_pomodoro); this.add_test ("reschedule_session__resume_session_1", this.test_reschedule_session__resume_session_1); this.add_test ("reschedule_session__resume_session_2", this.test_reschedule_session__resume_session_2); this.add_test ("reschedule_session__starting_with_long_break", this.test_reschedule_session__starting_with_long_break); this.add_test ("reschedule_session__in_progress_time_block", this.test_reschedule_session__in_progress_time_block); this.add_test ("reschedule_session__paused_pomodoro", this.test_reschedule_session__paused_pomodoro); this.add_test ("reschedule_session__paused_short_break", this.test_reschedule_session__paused_short_break); this.add_test ("reschedule_session__extended_pomodoro_1x", this.test_reschedule_session__extended_pomodoro_1x); this.add_test ("reschedule_session__extended_pomodoro_2x", this.test_reschedule_session__extended_pomodoro_2x); this.add_test ("ensure_session_meta__scheduled", this.test_ensure_session_meta__scheduled); this.add_test ("ensure_session_meta__completed", this.test_ensure_session_meta__completed); this.add_test ("ensure_session_meta__uncompleted", this.test_ensure_session_meta__uncompleted); this.add_test ("ensure_session_meta__in_progress", this.test_ensure_session_meta__in_progress); this.add_test ("ensure_session_meta__with_gaps", this.test_ensure_session_meta__with_gaps); this.add_test ("ensure_session_meta__mixed_states", this.test_ensure_session_meta__mixed_states); } public void test_calculate_time_block_completion_time () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 30 * Ft.Interval.SECOND); time_block_1.set_intended_duration (25 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block_1)), new GLib.Variant.int64 (now + 20 * Ft.Interval.MINUTE) ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now, now + 20 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (25 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block_2)), new GLib.Variant.int64 (now + 20 * Ft.Interval.MINUTE) ); var time_block_3 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_3.set_time_range (now, now + 50 * Ft.Interval.MINUTE); time_block_3.set_intended_duration (25 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block_3)), new GLib.Variant.int64 (now + 20 * Ft.Interval.MINUTE) ); var time_block_4 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_4.set_time_range (now, now + 10 * Ft.Interval.MINUTE); time_block_4.set_intended_duration (5 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block_4)), new GLib.Variant.int64 (now + 4 * Ft.Interval.MINUTE) ); var time_block_5 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_5.set_time_range (now, now + 5 * Ft.Interval.SECOND); time_block_5.set_intended_duration (5 * Ft.Interval.SECOND); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block_5)), new GLib.Variant.int64 (now + 4 * Ft.Interval.SECOND) ); } public void test_calculate_time_block_completion_time__with_gaps () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block)), new GLib.Variant.int64 (now + 20 * Ft.Interval.MINUTE) ); var gap = new Ft.Gap (); gap.set_time_range (now + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block)), new GLib.Variant.int64 (now + 20 * Ft.Interval.MINUTE) ); gap.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 15 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block)), new GLib.Variant.int64 (now + 30 * Ft.Interval.MINUTE) ); time_block.set_time_range (now, now + 35 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (scheduler.calculate_time_block_completion_time (time_block)), new GLib.Variant.int64 (now + 30 * Ft.Interval.MINUTE) ); } public void test_calculate_time_block_score__pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var score = 0.0; var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 20 * Ft.Interval.MINUTE); // At different timestamps time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); score = scheduler.calculate_time_block_score (time_block, now + 19 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); score = scheduler.calculate_time_block_score (time_block, now + 20 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); score = scheduler.calculate_time_block_score (time_block, now + 99 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); // After marking time-block end time time_block.set_status (Ft.TimeBlockStatus.COMPLETED); time_block.set_time_range (now, now + 19 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); time_block.set_time_range (now, now + 20 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + (25 + 19) * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + (25 + 20) * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); // Uncompleted status should have a priority time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); } public void test_calculate_time_block_score__extended_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var score = 0.0; var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 50 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 20 * Ft.Interval.MINUTE); // At different timestamps time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); score = scheduler.calculate_time_block_score (time_block, now + 44 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); score = scheduler.calculate_time_block_score (time_block, now + 45 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); score = scheduler.calculate_time_block_score (time_block, now + 50 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); // After marking time-block end time time_block.set_status (Ft.TimeBlockStatus.COMPLETED); time_block.set_time_range (now, now + 44 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + 45 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); time_block.set_time_range (now, now + 50 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); // Uncompleted status should have a priority time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); } public void test_calculate_time_block_score__short_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var score = 0.0; var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_intended_duration (15 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 20 * Ft.Interval.MINUTE); time_block.set_time_range (now, now + 11 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); time_block.set_time_range (now, now + 12 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + 30 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 1.0, EPSILON); time_block.set_time_range (now, now + 40 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 2.0, EPSILON); } public void test_calculate_time_block_score__short_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var score = 0.0; var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.set_time_range (now, now + 4 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); } public void test_calculate_time_block_score__long_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var score = 0.0; var time_block = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block.set_intended_duration (15 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.set_time_range (now, now + 12 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); time_block.set_time_range (now, now + 15 * Ft.Interval.MINUTE); score = scheduler.calculate_time_block_score (time_block, time_block.end_time); assert_cmpfloat_with_epsilon (score, 0.0, EPSILON); } public void test_calculate_time_block_score__uncompleted_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 20 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); time_block.duration = 1 * Ft.Interval.MINUTE; assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 0.0 ); time_block.duration = 20 * Ft.Interval.MINUTE; assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 0.0 ); time_block.duration = 25 * Ft.Interval.MINUTE; assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 0.0 ); } public void test_calculate_time_block_score__paused_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 20 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.start_time), GLib.CompareOperator.EQ, 0.0); // Start a pause var gap_1 = new Ft.Gap (); gap_1.set_time_range (now + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap_1); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, gap_1.start_time), GLib.CompareOperator.EQ, 0.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 0.0); // Resume. Check if a long pause confuses the scheduler. gap_1.duration = 30 * Ft.Interval.MINUTE; time_block.duration += gap_1.duration; time_block.set_completion_time (time_block.end_time - 5 * Ft.Interval.MINUTE); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, gap_1.end_time), GLib.CompareOperator.EQ, 0.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time + Ft.Interval.HOUR), GLib.CompareOperator.EQ, 1.0); // Pause after `completion_time` now = time_block.end_time - Ft.Interval.MINUTE; var gap_2 = new Ft.Gap (); gap_2.set_time_range (now, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap_2); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, gap_2.start_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time + Ft.Interval.HOUR), GLib.CompareOperator.EQ, 1.0); // Resume. Check if a long pause confuses the scheduler. gap_2.duration = 30 * Ft.Interval.MINUTE; time_block.duration += gap_2.duration; assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, gap_2.start_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, gap_2.end_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time), GLib.CompareOperator.EQ, 1.0); assert_cmpfloat ( scheduler.calculate_time_block_score (time_block, time_block.end_time + Ft.Interval.HOUR), GLib.CompareOperator.EQ, 1.0); } /** * Ignore ongoing gap when calculating weight. */ public void test_calculate_time_block_weight__paused_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_1.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_1), 1.0, EPSILON); var gap_1 = new Ft.Gap (); gap_1.set_time_range (now + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block_1.add_gap (gap_1); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_1), 1.0, EPSILON); gap_1.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 15 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_1), 0.0, EPSILON); time_block_1.set_time_range (now, now + 35 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_1), 1.0, EPSILON); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap_2 = new Ft.Gap (); gap_2.set_time_range (now + 20 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block_2.add_gap (gap_2); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_2), 1.0, EPSILON); var gap_3 = new Ft.Gap (); gap_3.set_time_range (now, now + 20 * Ft.Interval.MINUTE); time_block_2.add_gap (gap_3); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_2), 0.0, EPSILON); time_block_2.set_time_range ( time_block_2.start_time, time_block_2.end_time + 50 * Ft.Interval.MINUTE); assert_cmpfloat_with_epsilon ( scheduler.calculate_time_block_weight (time_block_2), 2.0, EPSILON); } /** * Time-block should complete at least 80% of intended duration, and not be shorter than 1 minute. */ public void test_is_time_block_completed__pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var timestamp_1 = time_block.start_time + 20 * Ft.Interval.MINUTE - Ft.Interval.SECOND; assert_false (scheduler.is_time_block_completed (time_block, timestamp_1)); var timestamp_2 = time_block.start_time + 20 * Ft.Interval.MINUTE; assert_true (scheduler.is_time_block_completed (time_block, timestamp_2)); } public void test_is_time_block_completed__short_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (1); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var timestamp_1 = time_block.start_time + 4 * Ft.Interval.MINUTE - Ft.Interval.SECOND; assert_false (scheduler.is_time_block_completed (time_block, timestamp_1)); var timestamp_2 = time_block.start_time + 4 * Ft.Interval.MINUTE; assert_true (scheduler.is_time_block_completed (time_block, timestamp_2)); } public void test_is_time_block_completed__long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_last_time_block (); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var timestamp_1 = time_block.start_time + 4 * Ft.Interval.MINUTE - Ft.Interval.SECOND; assert_false (scheduler.is_time_block_completed (time_block, timestamp_1)); var timestamp_2 = time_block.start_time + 12 * Ft.Interval.MINUTE; assert_true (scheduler.is_time_block_completed (time_block, timestamp_2)); } /** * Expect `Scheduler.resolve_state()` to copy current time-block state into scheduler context. */ public void test_resolve_context__update_state () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); Ft.State[] states = { Ft.State.STOPPED, Ft.State.POMODORO, Ft.State.SHORT_BREAK, Ft.State.LONG_BREAK }; foreach (var state in states) { var context = Ft.SchedulerContext (); var time_block = new Ft.TimeBlock (state); time_block.set_time_range (20, 30); time_block.set_intended_duration (time_block.duration); scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_true (context.state == state); } } public void test_resolve_context__update_timestamp () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); int64[] timestamps = { now + Ft.Interval.MINUTE, now + 5 * Ft.Interval.MINUTE, now + 10 * Ft.Interval.MINUTE }; foreach (var timestamp in timestamps) { var context = Ft.SchedulerContext (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + Ft.Interval.MINUTE); scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( new GLib.Variant.int64 (context.timestamp), new GLib.Variant.int64 (time_block.end_time) ); } } /** * Completing a pomodoro should mark session as completed. */ public void test_resolve_context__completed_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 9 * Ft.Interval.MINUTE); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 0.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.POMODORO, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__completed_short_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var context = Ft.SchedulerContext () { timestamp = time_block.start_time, state = Ft.State.STOPPED, score = 1.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.SHORT_BREAK, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } /** * Completing a long break should mark session as completed. */ public void test_resolve_context__completed_long_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block.set_time_range (now, now + 15 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var context = Ft.SchedulerContext () { timestamp = time_block.start_time, state = Ft.State.STOPPED, is_session_completed = false, needs_long_break = true, score = 4.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.LONG_BREAK, is_session_completed = true, needs_long_break = false, score = 4.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__uncompleted_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_time_range (now, now + 3 * Ft.Interval.MINUTE); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 0.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.POMODORO, score = 0.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__uncompleted_short_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 1.0, }; var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.SHORT_BREAK, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__uncompleted_long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, is_session_completed = false, needs_long_break = true, score = 4.0, }; var expected_context = Ft.SchedulerContext () { state = Ft.State.LONG_BREAK, is_session_completed = false, needs_long_break = true, score = 4.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__uncompleted_last_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, is_session_completed = false, needs_long_break = false, score = 3.0, }; var expected_context = Ft.SchedulerContext () { state = Ft.State.POMODORO, is_session_completed = false, needs_long_break = false, score = 3.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__in_progress_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 9 * Ft.Interval.MINUTE); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 4 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 0.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.POMODORO, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__in_progress_short_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 4 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 1.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.SHORT_BREAK, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__in_progress_long_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var cycles = (double) this.session_template.cycles; var time_block = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block.set_time_range (now, now + 15 * Ft.Interval.MINUTE); time_block.set_intended_duration (12 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 12 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, is_session_completed = false, needs_long_break = true, score = cycles, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.LONG_BREAK, is_session_completed = true, needs_long_break = false, score = cycles, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__paused_pomodoro () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 4 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap.with_start_time (now + 1 * Ft.Interval.MINUTE); time_block.add_gap (gap); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 0.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.POMODORO, score = 1.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__paused_short_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_completion_time (now + 4 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap.with_start_time (now + 1 * Ft.Interval.MINUTE); time_block.add_gap (gap); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, score = 0.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.SHORT_BREAK, score = 0.0, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_context__needs_long_break () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var cycles = (double) this.session_template.cycles; var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); time_block.set_intended_duration (5 * Ft.Interval.MINUTE); time_block.set_time_range (now, now + 4 * Ft.Interval.MINUTE); var context = Ft.SchedulerContext () { state = Ft.State.STOPPED, needs_long_break = false, score = cycles - 1.0, }; var expected_context = Ft.SchedulerContext () { timestamp = time_block.end_time, state = Ft.State.POMODORO, needs_long_break = true, score = cycles, }; scheduler.resolve_context (time_block, true, time_block.end_time, ref context); assert_cmpvariant ( context.to_variant (), expected_context.to_variant () ); } public void test_resolve_time_block__pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var context_1 = Ft.SchedulerContext () { state = Ft.State.STOPPED, }; var time_block_1 = scheduler.resolve_time_block (context_1); assert_true (time_block_1.state == Ft.State.POMODORO); var context_2 = Ft.SchedulerContext () { state = Ft.State.SHORT_BREAK, }; var time_block_2 = scheduler.resolve_time_block (context_2); assert_true (time_block_2.state == Ft.State.POMODORO); } public void test_resolve_time_block__short_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var context = Ft.SchedulerContext () { state = Ft.State.POMODORO, }; var time_block = scheduler.resolve_time_block (context); assert_true (time_block.state == Ft.State.SHORT_BREAK); } public void test_resolve_time_block__long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var context = Ft.SchedulerContext () { state = Ft.State.POMODORO, needs_long_break = true, }; var time_block = scheduler.resolve_time_block (context); assert_true (time_block.state == Ft.State.LONG_BREAK); } /** * Expect `resolve_time_block()` to return null for a completed session. */ public void test_resolve_time_block__completed_session () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var context = Ft.SchedulerContext () { state = Ft.State.LONG_BREAK, score = (double) this.session_template.cycles, is_session_completed = true, }; var time_block = scheduler.resolve_time_block (context); assert_null (time_block); } /** * Populate empty session using scheduler. */ public void test_reschedule_session__populate () { var timestamp = Ft.Timestamp.advance (0) + Ft.Interval.MINUTE; var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = new Ft.Session (); var session_changed_emitted = 0; session.changed.connect (() => { session_changed_emitted++; }); scheduler.reschedule_session (session, null, true, timestamp); assert_cmpuint (session.get_cycles ().length (), GLib.CompareOperator.EQ, this.session_template.cycles); assert_cmpvariant ( new GLib.Variant.int64 (session.start_time), new GLib.Variant.int64 (timestamp) ); var time_block_1 = session.get_nth_time_block (0); assert_true (time_block_1.state == Ft.State.POMODORO); var time_block_2 = session.get_nth_time_block (1); assert_true (time_block_2.state == Ft.State.SHORT_BREAK); var last_time_block = session.get_last_time_block (); assert_true (last_time_block.state == Ft.State.LONG_BREAK); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Rescheduling a session that has completed long-break shouldn't do anything. * * No upcoming time-block should force `SessionManager` to start new session. */ public void test_reschedule_session__completed_session () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); session.@foreach ( (time_block) => { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } ); scheduler.ensure_session_meta (session); var session_changed_emitted = 0; session.changed.connect (() => { session_changed_emitted++; }); scheduler.reschedule_session (session, null, true, session.end_time); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 0); } public void test_reschedule_session__uncompleted_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); time_block_1.duration = Ft.Interval.MINUTE; time_block_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var time_block_2 = session.get_nth_time_block (1); scheduler.ensure_session_meta (session); var now = time_block_1.end_time; scheduler.reschedule_session (session, null, true, now); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } public void test_reschedule_session__uncompleted_short_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); // Pomodoro time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var time_block_2 = session.get_nth_time_block (1); // Short break time_block_2.duration = Ft.Interval.MINUTE; time_block_2.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var time_block_3 = session.get_nth_time_block (2); // Pomodoro scheduler.ensure_session_meta (session); var now = time_block_2.end_time; scheduler.reschedule_session (session, null, true, now); assert_cmpvariant ( new GLib.Variant.int64 (time_block_3.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } public void test_reschedule_session__uncompleted_last_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var long_break_1 = session.get_last_time_block (); var last_pomodoro = session.get_previous_time_block (long_break_1); session.@foreach ( (time_block) => { if (time_block != last_pomodoro && time_block != long_break_1) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); last_pomodoro.duration = Ft.Interval.MINUTE; last_pomodoro.set_status (Ft.TimeBlockStatus.UNCOMPLETED); scheduler.ensure_session_meta (session); // Rescheule var now = last_pomodoro.end_time; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); var extra_short_break = session.get_next_time_block (last_pomodoro); assert_nonnull (extra_short_break); assert_cmpvariant ( new GLib.Variant.int64 (extra_short_break.start_time), new GLib.Variant.int64 (now) ); assert_true (extra_short_break.state == Ft.State.SHORT_BREAK); assert_true (extra_short_break.get_status () == Ft.TimeBlockStatus.SCHEDULED); var extra_pomodoro = session.get_next_time_block (extra_short_break); assert_nonnull (extra_pomodoro); assert_true (extra_pomodoro.state == Ft.State.POMODORO); var long_break_2 = session.get_next_time_block (extra_pomodoro); assert_nonnull (long_break_2); assert_true (long_break_2.state == Ft.State.LONG_BREAK); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); var last_cycle = session.get_cycles ().last ().data; assert_true (last_cycle.is_visible ()); } public void test_reschedule_session__uncompleted_long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break = session.get_last_time_block (); long_break.duration = Ft.Interval.MINUTE; long_break.set_status (Ft.TimeBlockStatus.UNCOMPLETED); scheduler.ensure_session_meta (session); // Rescheule var now = long_break.end_time; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); var extra_pomodoro = session.get_next_time_block (long_break); assert_nonnull (extra_pomodoro); assert_cmpvariant ( new GLib.Variant.int64 (extra_pomodoro.start_time), new GLib.Variant.int64 (now) ); assert_true (extra_pomodoro.state == Ft.State.POMODORO); assert_true (extra_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); var extra_long_break = session.get_next_time_block (extra_pomodoro); assert_nonnull (extra_long_break); assert_cmpvariant ( new GLib.Variant.int64 (extra_long_break.start_time), new GLib.Variant.int64 (extra_pomodoro.end_time) ); assert_true (extra_long_break.state == Ft.State.LONG_BREAK); assert_true (extra_long_break.get_status () == Ft.TimeBlockStatus.SCHEDULED); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); var extra_cycle = session.get_cycles ().last ().data; assert_true (extra_cycle.is_visible ()); } /** * Run reschedule after stopping extra Ft. * * Expect extra cycle as as we haven't completed a long-break. */ public void test_reschedule_session__uncompleted_extra_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break_1 = session.get_last_time_block (); long_break_1.end_time = long_break_1.start_time + Ft.Interval.MINUTE; long_break_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var extra_pomodoro = new Ft.TimeBlock (Ft.State.POMODORO); extra_pomodoro.set_time_range ( long_break_1.end_time, long_break_1.end_time + 25 * Ft.Interval.MINUTE); extra_pomodoro.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var long_break_2 = new Ft.TimeBlock (Ft.State.LONG_BREAK); long_break_2.set_time_range ( extra_pomodoro.end_time, extra_pomodoro.end_time + 15 * Ft.Interval.MINUTE); long_break_2.set_status (Ft.TimeBlockStatus.SCHEDULED); session.append (extra_pomodoro); session.append (long_break_2); scheduler.ensure_session_meta (session); // Skip long-break var now = long_break_1.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); long_break_1.end_time = now; long_break_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); } /** * Simulate skipping an uncompleted long-break. * * Expect extra cycle. */ public void test_reschedule_session__skip_uncompleted_long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break_1 = session.get_last_time_block (); long_break_1.set_status (Ft.TimeBlockStatus.IN_PROGRESS); scheduler.ensure_session_meta (session); // Skip long-break var now = long_break_1.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); long_break_1.end_time = now; long_break_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var extra_pomodoro = session.get_next_time_block (long_break_1); assert_nonnull (extra_pomodoro); assert_true (extra_pomodoro.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (extra_pomodoro.start_time), new GLib.Variant.int64 (now) ); var long_break_2 = session.get_next_time_block (extra_pomodoro); assert_nonnull (long_break_2); assert_true (long_break_2 != long_break_1); assert_true (long_break_2.state == Ft.State.LONG_BREAK); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); } /** * Run reschedule after skipping extra Ft. * * We start an extra cycle, but skip it shortly after. * Expect the number of visible cycles to be reduced, as we jump to a long-break. */ public void test_reschedule_session__skip_uncompleted_extra_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break_1 = session.get_last_time_block (); long_break_1.end_time = long_break_1.start_time + Ft.Interval.MINUTE; long_break_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var extra_pomodoro = new Ft.TimeBlock (Ft.State.POMODORO); extra_pomodoro.set_time_range ( long_break_1.end_time, long_break_1.end_time + 25 * Ft.Interval.MINUTE); extra_pomodoro.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var long_break_2 = new Ft.TimeBlock (Ft.State.LONG_BREAK); long_break_2.set_time_range ( extra_pomodoro.end_time, extra_pomodoro.end_time + 15 * Ft.Interval.MINUTE); long_break_2.set_status (Ft.TimeBlockStatus.SCHEDULED); session.append (extra_pomodoro); session.append (long_break_2); scheduler.ensure_session_meta (session); // Skip extra pomodoro var now = extra_pomodoro.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, long_break_2, true, now); extra_pomodoro.end_time = now; extra_pomodoro.set_status (Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (long_break_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Simulate resuming session after stopping the timer. * * Simplest case, where we continue after a minute with the next time-block. */ public void test_reschedule_session__resume_session_1 () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); time_block_1.duration = Ft.Interval.MINUTE; time_block_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var time_block_2 = session.get_nth_time_block (1); // Prepare session before entering time_block_2 var now = time_block_1.end_time + Ft.Interval.MINUTE; scheduler.reschedule_session (session, null, true, now); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); // Enter time_block_2 time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); scheduler.reschedule_session (session, null, true, now); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); // Ensure that `reschedule_session` behaves OK while time_block_2 is in progress scheduler.reschedule_session (session, null, true, now + Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Simulate resuming session after stopping the timer. * * Real-world use, where we insert a new Ft. */ public void test_reschedule_session__resume_session_2 () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); time_block_1.duration = Ft.Interval.MINUTE; time_block_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); // Simulate `initialize_next_time_block` var now = time_block_1.end_time + 30 * Ft.Interval.MINUTE; var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now, now + 25 * Ft.Interval.MINUTE); session.insert_after (time_block_2, time_block_1); scheduler.reschedule_session (session, time_block_2, true, now); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } public void test_reschedule_session__starting_with_long_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_first_time_block (); time_block_1.duration = Ft.Interval.MINUTE; time_block_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var time_block_2 = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block_2.set_time_range (time_block_1.end_time, time_block_1.end_time + 15 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.SCHEDULED); time_block_2.set_intended_duration (15 * Ft.Interval.MINUTE); time_block_2.set_completion_time (time_block_2.end_time); session.insert_after (time_block_2, time_block_1); Ft.Timestamp.freeze_to (time_block_2.start_time); scheduler.reschedule_session (session, time_block_2, true, time_block_2.start_time); assert_cmpuint ( session.count_time_blocks (), GLib.CompareOperator.EQ, 2U ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, 0U ); } /** * Test rescheduling a session with in-progress time-blocks. */ public void test_reschedule_session__in_progress_time_block () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); // Pomodoro time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); time_block_1.set_intended_duration (time_block_1.duration); time_block_1.set_weight (1.0); var time_block_2 = session.get_nth_time_block (1); // Short break time_block_2.set_status (Ft.TimeBlockStatus.COMPLETED); time_block_2.set_intended_duration (time_block_2.duration); time_block_2.set_weight (0.0); var time_block_3 = session.get_nth_time_block (2); // Pomodoro time_block_3.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block_3.set_intended_duration (time_block_3.duration); time_block_3.set_weight (1.0); // Reschedule session var now = time_block_3.end_time - Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); var pomodoros_count = session.count_time_blocks ( time_block => time_block.state == Ft.State.POMODORO); assert_cmpuint (pomodoros_count, GLib.CompareOperator.EQ, 4U); assert_cmpfloat_with_epsilon ( time_block_3.get_weight (), 1.0, EPSILON ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Treat in-progress time-blocks as if they're going to be completed according to schedule. * * Note that we're passing `time_block.end_time` for a timestamp. */ public void test_reschedule_session__paused_pomodoro () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); // Pomodoro time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.set_intended_duration (time_block.duration); time_block.set_weight (1.0); var gap = new Ft.Gap.with_start_time ( time_block.start_time + Ft.Interval.MINUTE); time_block.add_gap (gap); // Reschedule session var timestamp = gap.start_time + 5 * Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (timestamp); scheduler.reschedule_session (session, null, true, time_block.end_time); assert_cmpfloat_with_epsilon ( time_block.get_weight (), 1.0, EPSILON ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } public void test_reschedule_session__paused_short_break () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); // Pomodoro time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); time_block_1.set_intended_duration (time_block_1.duration); time_block_1.set_weight (1.0); var time_block_2 = session.get_nth_time_block (1); // Short break time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block_2.set_intended_duration (time_block_2.duration); time_block_2.set_weight (0.0); var gap = new Ft.Gap.with_start_time ( time_block_2.start_time + Ft.Interval.MINUTE); time_block_2.add_gap (gap); // Reschedule session var timestamp = gap.start_time + 5 * Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (timestamp); scheduler.reschedule_session (session, null, true, time_block_2.end_time); assert_cmpfloat_with_epsilon ( time_block_2.get_weight (), 0.0, EPSILON ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Test rescheduling a session after a pomodoro has been extended by 1 minute. * * The extension is small, so the weight should remain 1.0 and visible cycles unchanged. */ public void test_reschedule_session__extended_pomodoro_1x () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); // Pomodoro time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.set_intended_duration (this.session_template.pomodoro_duration); var now = time_block.end_time - 10 * Ft.Interval.SECOND; time_block.end_time = now + Ft.Interval.MINUTE; time_block.set_weight (scheduler.calculate_time_block_weight (time_block)); Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); assert_cmpfloat ( time_block.get_weight (), GLib.CompareOperator.EQ, 1.0 ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Test rescheduling a session after a pomodoro has been extended, doubling its duration. * * The pomodoro is extended from 25 to 50 minutes (2x intended duration). * This should result in weight = 2.0 and one less visible cycle. */ public void test_reschedule_session__extended_pomodoro_2x () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); // Pomodoro time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.set_intended_duration (this.session_template.pomodoro_duration); var now = time_block.end_time - 10 * Ft.Interval.SECOND; time_block.end_time = now + time_block.duration; time_block.set_weight (scheduler.calculate_time_block_weight (time_block)); Ft.Timestamp.freeze_to (now); scheduler.reschedule_session (session, null, true, now); assert_cmpfloat ( time_block.get_weight (), GLib.CompareOperator.EQ, 2.0 ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles - 1 ); } /** * Test restoring a session with all scheduled time-blocks. * * Expect meta fields to be updated for all time-blocks. */ public void test_ensure_session_meta__scheduled () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = new Ft.Session.from_template (this.session_template); scheduler.ensure_session_meta (session); session.@foreach ( (time_block) => { var meta = time_block.get_meta (); if (time_block.state == Ft.State.POMODORO) { assert_cmpvariant ( new GLib.Variant.int64 (meta.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); } else if (time_block.state == Ft.State.SHORT_BREAK) { assert_cmpvariant ( new GLib.Variant.int64 (meta.intended_duration), new GLib.Variant.int64 (this.session_template.short_break_duration) ); } else if (time_block.state == Ft.State.LONG_BREAK) { assert_cmpvariant ( new GLib.Variant.int64 (meta.intended_duration), new GLib.Variant.int64 (this.session_template.long_break_duration) ); } assert_true (Ft.Timestamp.is_defined (meta.completion_time)); assert_true (meta.completion_time > time_block.start_time); assert_true (meta.completion_time <= time_block.end_time); } ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Test restoring session meta with completed time-blocks. * * Expect meta fields to be updated for completed time-blocks. */ public void test_ensure_session_meta__completed () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); time_block_2.set_status (Ft.TimeBlockStatus.COMPLETED); scheduler.ensure_session_meta (session); var meta_1 = time_block_1.get_meta (); assert_true (Ft.Timestamp.is_defined (meta_1.completion_time)); assert_cmpvariant ( new GLib.Variant.int64 (meta_1.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); var meta_2 = time_block_2.get_meta (); assert_true (Ft.Timestamp.is_defined (meta_2.completion_time)); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Test restoring session meta with uncompleted time-blocks. */ public void test_ensure_session_meta__uncompleted () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); time_block.set_status (Ft.TimeBlockStatus.UNCOMPLETED); scheduler.ensure_session_meta (session); var meta = time_block.get_meta (); assert_true (Ft.Timestamp.is_defined (meta.completion_time)); assert_cmpvariant ( new GLib.Variant.int64 (meta.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles - 1 ); } /** * Test restoring session meta with in-progress time-blocks. */ public void test_ensure_session_meta__in_progress () { var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = this.create_session (scheduler); var time_block = session.get_nth_time_block (0); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); scheduler.ensure_session_meta (session); var meta = time_block.get_meta (); assert_true (Ft.Timestamp.is_defined (meta.completion_time)); assert_cmpvariant ( new GLib.Variant.int64 (meta.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Test restoring session meta with time-blocks that have gaps. * * Expect completion time to account for gaps. */ public void test_ensure_session_meta__with_gaps () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = new Ft.Session (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 35 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (); gap.set_time_range (now + 5 * Ft.Interval.MINUTE, now + 15 * Ft.Interval.MINUTE); time_block.add_gap (gap); session.append (time_block); scheduler.ensure_session_meta (session); var meta = time_block.get_meta (); // Completion time should be: start + 20 minutes (80% of 25) + 10 minutes (gap duration) assert_cmpvariant ( new GLib.Variant.int64 (meta.completion_time), new GLib.Variant.int64 (now + 30 * Ft.Interval.MINUTE) ); assert_cmpuint (session.count_visible_cycles (), GLib.CompareOperator.EQ, 1U); } /** * Test restoring session meta with mixed time-block states and edge cases. * * This is a more realistic test with: * - Some completed pomodoros with different durations * - An uncompleted pomodoro * - An in-progress break * - Scheduled time-blocks * - Time-blocks with gaps */ public void test_ensure_session_meta__mixed_states () { var now = Ft.Timestamp.peek (); var scheduler = new Ft.SimpleScheduler.with_template (this.session_template); var session = new Ft.Session (); // Completed pomodoro with normal duration var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); session.append (time_block_1); // Completed short break var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (time_block_1.end_time, time_block_1.end_time + 5 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.COMPLETED); session.append (time_block_2); // Uncompleted pomodoro (was interrupted early) var time_block_3 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_3.set_time_range (time_block_2.end_time, time_block_2.end_time + 10 * Ft.Interval.MINUTE); time_block_3.set_status (Ft.TimeBlockStatus.UNCOMPLETED); session.append (time_block_3); // In-progress pomodoro with a gap (pause) var time_block_4 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_4.set_time_range (time_block_3.end_time, time_block_3.end_time + 30 * Ft.Interval.MINUTE); time_block_4.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (); gap.set_time_range (time_block_4.start_time + 10 * Ft.Interval.MINUTE, time_block_4.start_time + 15 * Ft.Interval.MINUTE); time_block_4.add_gap (gap); session.append (time_block_4); // Scheduled short break var time_block_5 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_5.set_time_range (time_block_4.end_time, time_block_4.end_time + 5 * Ft.Interval.MINUTE); time_block_5.set_status (Ft.TimeBlockStatus.SCHEDULED); session.append (time_block_5); // Restore session scheduler.ensure_session_meta (session); // Verify all time-blocks have meta fields properly set var meta_1 = time_block_1.get_meta (); assert_cmpvariant ( new GLib.Variant.int64 (meta_1.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); assert_true (Ft.Timestamp.is_defined (meta_1.completion_time)); var meta_2 = time_block_2.get_meta (); assert_cmpvariant ( new GLib.Variant.int64 (meta_2.intended_duration), new GLib.Variant.int64 (this.session_template.short_break_duration) ); assert_true (Ft.Timestamp.is_defined (meta_2.completion_time)); // Uncompleted pomodoro should still have meta updated var meta_3 = time_block_3.get_meta (); assert_cmpvariant ( new GLib.Variant.int64 (meta_3.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); assert_true (Ft.Timestamp.is_defined (meta_3.completion_time)); // In-progress with gap should have completion time adjusted for gap var meta_4 = time_block_4.get_meta (); assert_cmpvariant ( new GLib.Variant.int64 (meta_4.intended_duration), new GLib.Variant.int64 (this.session_template.pomodoro_duration) ); assert_true (Ft.Timestamp.is_defined (meta_4.completion_time)); // Completion should be after the gap ends assert_true (meta_4.completion_time > gap.end_time); // Scheduled break var meta_5 = time_block_5.get_meta (); assert_cmpvariant ( new GLib.Variant.int64 (meta_5.intended_duration), new GLib.Variant.int64 (this.session_template.short_break_duration) ); assert_true (Ft.Timestamp.is_defined (meta_5.completion_time)); // Verify session structure is intact (no time-blocks removed) Ft.TimeBlock[] time_blocks = {}; session.@foreach ((time_block) => { time_blocks += time_block; }); assert_cmpuint (time_blocks.length, GLib.CompareOperator.EQ, 5); assert_cmpuint (session.count_visible_cycles (), GLib.CompareOperator.EQ, 2U); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.SimpleSchedulerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-session-manager.vala000066400000000000000000007525531520625676500247570ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private double EPSILON = 0.0001; public class SessionManagerTest : Tests.TestSuite { private Ft.Timer timer; private Ft.SessionTemplate session_template = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 4 }; public SessionManagerTest () { this.add_test ("new", this.test_new); this.add_test ("new_with_timer", this.test_new_with_timer); this.add_test ("set_current_session", this.test_set_current_session); this.add_test ("set_current_session__while_entering_session", this.test_set_current_session__while_entering_session); this.add_test ("set_current_session__while_leaving_session", this.test_set_current_session__while_leaving_session); this.add_test ("set_current_session__while_entering_time_block", this.test_set_current_session__while_entering_time_block); this.add_test ("set_current_session__while_leaving_time_block", this.test_set_current_session__while_leaving_time_block); this.add_test ("set_current_session__null", this.test_set_current_session__null); this.add_test ("set_current_time_block", this.test_set_current_time_block); this.add_test ("set_current_time_block__while_entering_session", this.test_set_current_time_block__while_entering_session); this.add_test ("set_current_time_block__while_leaving_session", this.test_set_current_time_block__while_leaving_session); this.add_test ("set_current_time_block__while_entering_time_block", this.test_set_current_time_block__while_entering_time_block); this.add_test ("set_current_time_block__while_leaving_time_block", this.test_set_current_time_block__while_leaving_time_block); this.add_test ("set_current_time_block__null", this.test_set_current_time_block__null); this.add_test ("set_current_time_block__with_new_session", this.test_set_current_time_block__with_new_session); this.add_test ("set_current_time_block__in_progress", this.test_set_current_time_block__in_progress); this.add_test ("set_current_time_block__in_progress_with_gaps", this.test_set_current_time_block__in_progress_with_gaps); this.add_test ("set_scheduler", this.test_set_scheduler); this.add_test ("advance__pomodoro", this.test_advance__pomodoro); this.add_test ("advance__paused_pomodoro", this.test_advance__paused_pomodoro); this.add_test ("advance__uncompleted_last_pomodoro", this.test_advance__uncompleted_last_pomodoro); this.add_test ("advance__uncompleted_long_break", this.test_advance__uncompleted_long_break); this.add_test ("advance__completed_long_break", this.test_advance__completed_long_break); this.add_test ("advance__uniform_breaks", this.test_advance__uniform_breaks); this.add_test ("advance_to_state__pomodoro", this.test_advance_to_state__pomodoro); this.add_test ("advance_to_state__short_break", this.test_advance_to_state__short_break); this.add_test ("advance_to_state__stopped", this.test_advance_to_state__stopped); this.add_test ("advance_to_state__extend_pomodoro", this.test_advance_to_state__extend_pomodoro); this.add_test ("advance_to_state__extend_short_break", this.test_advance_to_state__extend_short_break); this.add_test ("advance_to_state__extend_long_break", this.test_advance_to_state__extend_long_break); this.add_test ("advance_to_state__switch_breaks", this.test_advance_to_state__switch_breaks); this.add_test ("advance_to_state__confirm_advancement", this.test_advance_to_state__confirm_advancement); this.add_test ("advance_to_state__skip_advancement", this.test_advance_to_state__skip_advancement); this.add_test ("advance_to_state__completed_session", this.test_advance_to_state__completed_session); this.add_test ("advance_to_state__uncompleted_session", this.test_advance_to_state__uncompleted_session); this.add_test ("confirm_starting_break", this.test_confirm_starting_break); this.add_test ("confirm_starting_pomodoro", this.test_confirm_starting_pomodoro); this.add_test ("reset__empty_session", this.test_reset__empty_session); this.add_test ("reset", this.test_reset); this.add_test ("expire_session__after_timeout", this.test_expire_session__after_timeout); this.add_test ("expire_session__after_suspend", this.test_expire_session__after_suspend); this.add_test ("settings_change", this.test_settings_change); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); } public override void teardown () { var settings = Ft.get_settings (); settings.revert (); Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); } public Ft.Session create_session (Ft.SessionManager session_manager) { var scheduler = session_manager.scheduler; var session = new Ft.Session.from_template (scheduler.session_template); session.@foreach ( (time_block) => { time_block.set_intended_duration (time_block.duration); time_block.set_completion_time (scheduler.calculate_time_block_completion_time (time_block)); } ); return session; } /* * Tests for constructors */ public void test_new () { var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1); settings.set_uint ("short-break-duration", 2); settings.set_uint ("long-break-duration", 3); settings.set_uint ("cycles", 5); var session_manager = new Ft.SessionManager (); assert_true (session_manager.timer == this.timer); assert_true (session_manager.timer.is_default ()); assert_false (session_manager.timer.is_started ()); assert_false (session_manager.timer.is_running ()); assert_null (session_manager.current_session); assert_null (session_manager.current_time_block); assert_cmpvariant ( session_manager.scheduler.session_template.to_variant (), Ft.SessionTemplate.with_defaults ().to_variant () ); } public void test_new_with_timer () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); assert_true (session_manager.timer == timer); assert_false (session_manager.timer.is_default ()); assert_false (session_manager.timer.is_started ()); assert_false (session_manager.timer.is_running ()); assert_null (session_manager.current_session); assert_null (session_manager.current_time_block); } /* * Tests for current-session property */ public void test_set_current_session () { var timer = this.timer; var session_manager = new Ft.SessionManager.with_timer (this.timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var resolve_state_emitted = 0; var state_changed_emitted = 0; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); timer.resolve_state.connect (() => { resolve_state_emitted++; }); timer.state_changed.connect (() => { state_changed_emitted++; }); var session_1 = new Ft.Session (); var session_2 = new Ft.Session.from_template (this.session_template); // Set empty session. Expect session to be set as current despite having no time-blocks. session_manager.current_session = session_1; assert_true (session_manager.current_session == session_1); assert_null (session_manager.current_time_block); assert_cmpstrv (signals, {"enter-session"}); signals.resize (0); // Set non-empty session. Expect current-time-block to become null. session_manager.current_session = session_2; assert_true (session_manager.current_session == session_2); assert_null (session_manager.current_time_block); assert_cmpstrv (signals, { "leave-session", "enter-session" }); assert_null (timer.user_data); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (0 * Ft.Interval.SECOND) ); assert_false (timer.is_started ()); assert_false (timer.is_running ()); signals.resize (0); // Set current-session with same session. Expect to it to be ignored. session_manager.current_session = session_manager.current_session; assert_cmpstrv (signals, {}); // Set current-session to null. session_manager.current_session = null; assert_cmpstrv (signals, { "leave-session" }); assert_null (timer.user_data); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (0 * Ft.Interval.SECOND) ); assert_false (timer.is_started ()); assert_false (timer.is_running ()); signals.resize (0); } public void test_set_current_session__while_entering_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); session_manager.enter_session.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_session = session_2; } }); session_manager.current_session = session_1; assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "enter-session", "leave-session", "enter-session" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 0); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 2); } public void test_set_current_session__while_leaving_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); var session_3 = new Ft.Session.from_template (this.session_template); session_manager.current_session = session_1; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.leave_session.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_session = session_3; } }); session_manager.current_session = session_2; assert_true (session_manager.current_session == session_3); assert_null (session_manager.current_time_block); assert_cmpstrv (signals, { "leave-session", "enter-session" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 0); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); } public void test_set_current_session__while_entering_time_block () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); session_manager.current_time_block = session_1.get_first_time_block (); assert_cmpuint (session_manager.ref_count, GLib.CompareOperator.EQ, 1); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.enter_time_block.connect ((session_manager_, time_block) => { if (!handler_called) { handler_called = true; session_manager_.current_session = session_2; } }); session_manager.current_time_block = session_1.get_nth_time_block (1); assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "leave-time-block", "enter-time-block", "leave-time-block", "leave-session", "enter-session" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 2); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); wait_for_object_finalized ((owned) session_manager); } public void test_set_current_session__while_leaving_time_block () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); session_manager.current_time_block = session_1.get_first_time_block (); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.leave_time_block.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_session = session_2; } }); session_manager.current_time_block = session_1.get_nth_time_block (1); assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "leave-time-block", "leave-session", "enter-session" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); wait_for_object_finalized ((owned) session_manager); } public void test_set_current_session__null () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var session = new Ft.Session.from_template (this.session_template); session_manager.current_time_block = session.get_first_time_block (); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.current_session = null; assert_null (session_manager.current_time_block); assert_null (session_manager.current_session); assert_cmpstrv (signals, { "leave-time-block", "leave-session" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); assert_false (timer.is_started ()); wait_for_object_finalized ((owned) session_manager); } /* * Tests for current-time-block property */ public void test_set_current_time_block () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); var session_1 = new Ft.Session.from_template (this.session_template); var time_block_1 = session_1.get_first_time_block (); var time_block_2 = session_1.get_next_time_block (time_block_1); var session_2 = new Ft.Session.from_template (this.session_template); var time_block_3 = session_2.get_first_time_block (); // Set empty session. Expect to set session as current, despite having no time-block yet. session_manager.current_time_block = time_block_1; assert_true (session_manager.current_time_block == time_block_1); assert_true (session_manager.current_session == session_1); assert_cmpstrv (signals, { "enter-session", "enter-time-block" }); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); signals.resize (0); notify_current_time_block_emitted = 0; notify_current_session_emitted = 0; // Set current time-block. Expect signals not to be emitted session_manager.current_time_block = session_manager.current_time_block; assert_cmpstrv (signals, {}); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 0); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 0); signals.resize (0); notify_current_time_block_emitted = 0; notify_current_session_emitted = 0; // Set current-time-block within same session. session_manager.current_time_block = time_block_2; assert_true (session_manager.current_time_block == time_block_2); assert_true (session_manager.current_session == session_1); assert_cmpstrv (signals, { "leave-time-block", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 0); signals.resize (0); notify_current_time_block_emitted = 0; notify_current_session_emitted = 0; // Set current-time-block with new session. session_manager.current_time_block = time_block_3; assert_true (session_manager.current_time_block == time_block_3); assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "leave-time-block", "leave-session", "enter-session", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); signals.resize (0); notify_current_time_block_emitted = 0; notify_current_session_emitted = 0; // Set current-time-block to null. Expect session not to be changed. session_manager.current_time_block = null; assert_null (session_manager.current_time_block); assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "leave-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 0); signals.resize (0); notify_current_time_block_emitted = 0; notify_current_session_emitted = 0; } public void test_set_current_time_block__while_entering_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); session_manager.enter_session.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_time_block = session_2.get_first_time_block (); } }); session_manager.current_session = session_1; assert_true (session_manager.current_session == session_2); assert_cmpstrv (signals, { "enter-session", "leave-session", "enter-session", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 2); } public void test_set_current_time_block__while_leaving_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; var session_1 = new Ft.Session.from_template (this.session_template); var session_2 = new Ft.Session.from_template (this.session_template); var session_3 = new Ft.Session.from_template (this.session_template); session_manager.current_session = session_1; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.leave_session.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_time_block = session_3.get_first_time_block (); } }); session_manager.current_session = session_2; assert_true (session_manager.current_time_block == session_3.get_first_time_block ()); assert_cmpstrv (signals, { "leave-session", "enter-session", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); } public void test_set_current_time_block__while_entering_time_block () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); session_manager.enter_time_block.connect ((session_manager_, session) => { if (!handler_called) { handler_called = true; session_manager_.current_time_block = time_block_2; } }); session_manager.current_time_block = time_block_1; assert_true (session_manager.current_time_block == time_block_2); assert_cmpstrv (signals, { "enter-session", "enter-time-block", "leave-time-block", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 2); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 1); } public void test_set_current_time_block__while_leaving_time_block () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var handler_called = false; var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); var time_block_3 = session.get_nth_time_block (2); session_manager.current_time_block = time_block_1; session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect ((session_manager_, session) => { signals += "leave-time-block"; if (!handler_called) { assert_true (session_manager_.current_time_block == time_block_1); handler_called = true; session_manager_.current_time_block = time_block_3; } }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); // Expect switching to `time_block_2` to be interrupted session_manager.current_time_block = time_block_2; assert_true (session_manager.current_time_block == time_block_3); assert_cmpstrv (signals, { "leave-time-block", "enter-time-block" }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 0); } public void test_set_current_time_block__null () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var notify_current_time_block_emitted = 0; var notify_current_session_emitted = 0; var session = new Ft.Session.from_template (this.session_template); session_manager.current_time_block = session.get_first_time_block (); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.notify["current-session"].connect (() => { notify_current_session_emitted++; }); session_manager.notify["current-time-block"].connect (() => { notify_current_time_block_emitted++; }); session_manager.current_time_block = null; assert_null (session_manager.current_time_block); assert_true (session_manager.current_session == session); assert_cmpstrv (signals, { "leave-time-block", }); assert_cmpint (notify_current_time_block_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (notify_current_session_emitted, GLib.CompareOperator.EQ, 0); assert_false (timer.is_started ()); } public void test_set_current_time_block__with_new_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block = session.get_first_time_block (); session_manager.current_time_block = time_block; assert_true (session_manager.current_session == session); assert_true (session_manager.current_time_block == time_block); } /** * Allow setting an in-progress time-block, as it's used by restore */ public void test_set_current_time_block__in_progress () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block = session.get_first_time_block (); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.notify["start-time"].connect ( () => { assert_not_reached (); }); var timestamp = time_block.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (timestamp); var expected_start_time = time_block.start_time; var expected_elapsed = time_block.calculate_elapsed (timestamp); var expected_remaining = time_block.calculate_remaining (timestamp); var expected_timer_state = Ft.TimerState () { duration = time_block.duration, offset = 0, started_time = time_block.start_time, paused_time = Ft.Timestamp.UNDEFINED, finished_time = Ft.Timestamp.UNDEFINED, user_data = time_block }; session_manager.current_time_block = time_block; assert_true (session_manager.current_session == session); assert_true (session_manager.current_time_block == time_block); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_null (time_block.get_last_gap ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (timestamp)), new GLib.Variant.int64 (expected_elapsed) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (timestamp)), new GLib.Variant.int64 (expected_remaining) ); assert_cmpvariant ( timer.state.to_variant (), expected_timer_state.to_variant () ); } public void test_set_current_time_block__in_progress_with_gaps () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block = session.get_first_time_block (); time_block.set_intended_duration (time_block.duration); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap_1 = new Ft.Gap (); gap_1.set_time_range (time_block.start_time + Ft.Interval.MINUTE, time_block.start_time + 3 * Ft.Interval.MINUTE); time_block.add_gap (gap_1); time_block.end_time += gap_1.duration; var gap_2 = new Ft.Gap.with_start_time ( time_block.start_time + 4 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); var timestamp = gap_2.start_time + 5 * Ft.Interval.MINUTE; var expected_start_time = time_block.start_time; var expected_elapsed = 2 * Ft.Interval.MINUTE; var expected_remaining = time_block.get_intended_duration () - expected_elapsed; var expected_cycles = session_manager.scheduler.session_template.cycles; var expected_timer_state = Ft.TimerState () { duration = time_block.get_intended_duration (), offset = gap_2.start_time - time_block.start_time - expected_elapsed, started_time = time_block.start_time, paused_time = gap_2.start_time, finished_time = Ft.Timestamp.UNDEFINED, user_data = time_block }; Ft.Timestamp.freeze_to (timestamp); session_manager.current_time_block = time_block; assert_true (session_manager.current_session == session); assert_true (session_manager.current_time_block == time_block); assert_true (session_manager.current_gap == gap_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (timestamp)), new GLib.Variant.int64 (expected_elapsed) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (timestamp)), new GLib.Variant.int64 (expected_remaining) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_true (time_block.get_nth_gap (0) == gap_1); assert_true (time_block.get_nth_gap (1) == gap_2); assert_true (Ft.Timestamp.is_undefined (gap_2.end_time)); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, expected_cycles ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (timestamp)), new GLib.Variant.int64 (expected_elapsed) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (timestamp)), new GLib.Variant.int64 (expected_remaining) ); assert_cmpvariant ( timer.state.to_variant (), expected_timer_state.to_variant () ); } /* * Tests for scheduler property */ public void test_set_scheduler () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var signals = new string[0]; var session = new Ft.Session.from_template (this.session_template); session_manager.current_time_block = session.get_first_time_block (); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session_manager.notify["scheduler"].connect (() => { signals += "notify::scheduler"; }); var scheduler_1 = session_manager.scheduler; session_manager.scheduler = scheduler_1; assert_true (session_manager.scheduler == scheduler_1); assert_cmpstrv (signals, {}); var scheduler_2 = new Ft.SimpleScheduler.with_template ( Ft.SessionTemplate () { pomodoro_duration = 30 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 20 * Ft.Interval.MINUTE, cycles = 4 } ); var expected_session = session_manager.current_session; var expected_time_block = session_manager.current_time_block; session_manager.scheduler = scheduler_2; assert_true (session_manager.scheduler == scheduler_2); assert_true (session_manager.current_session == expected_session); assert_true (session_manager.current_time_block == expected_time_block); assert_cmpstrv (signals, { "session-rescheduled", "notify::scheduler" }); } /* * Tests for advance_* methods */ public void test_advance__pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session_changed_emitted = 0; var signals = new string[0]; // Start timer var now = Ft.Timestamp.peek (); timer.start (now); var session = session_manager.current_session; var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.get_completion_time ()), new GLib.Variant.int64 (session_manager.scheduler.calculate_time_block_completion_time (time_block_1)) ); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); // Skip to a short-break after a minute timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance (now); assert_true (session_manager.current_session == session); assert_false (session_manager.current_time_block == time_block_1); assert_true (session_manager.current_time_block == time_block_2); assert_true (time_block_1.state == Ft.State.POMODORO); assert_true (time_block_2.state == Ft.State.SHORT_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_true (time_block_2.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "session-rescheduled", "leave-time-block", "enter-time-block", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Skipping while being paused should resume the timer */ public void test_advance__paused_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var session = session_manager.current_session; var time_block = session_manager.current_time_block; var pause_time = time_block.start_time + 3 * Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (pause_time); timer.pause (pause_time); assert_true (timer.is_paused ()); var advance_time = pause_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (advance_time); session_manager.advance (advance_time); assert_false (timer.is_paused ()); var gap = time_block.get_last_gap (); assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (pause_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (advance_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (advance_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_time_block.start_time), new GLib.Variant.int64 (advance_time) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * Skipping last pomodoro without completing it should schedule another cycle. * However, the number of visible cycles should not change. */ public void test_advance__uncompleted_last_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var cycles = session.count_visible_cycles (); var long_break = session.get_last_time_block (); assert_true (long_break.state == Ft.State.LONG_BREAK); var last_pomodoro = session.get_previous_time_block (long_break); assert_true (last_pomodoro.state == Ft.State.POMODORO); session.@foreach ( (time_block) => { if (time_block != last_pomodoro && time_block != long_break) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); Ft.Timestamp.freeze_to (last_pomodoro.start_time); session_manager.current_time_block = last_pomodoro; var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance (now); assert_true (last_pomodoro.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); var scheduled_break = session_manager.current_time_block; assert_nonnull (scheduled_break); assert_true (scheduled_break.state == Ft.State.SHORT_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (scheduled_break.duration), new GLib.Variant.int64 (this.session_template.short_break_duration) ); var scheduled_pomodoro = session.get_next_time_block (scheduled_break); assert_nonnull (scheduled_pomodoro); assert_true (scheduled_pomodoro.state == Ft.State.POMODORO); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, cycles ); } /** * Skipping a long break without completing it should add extra cycle. */ public void test_advance__uncompleted_long_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var cycles = session.get_cycles ().length (); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var time_block = session_manager.current_session.get_last_time_block (); Ft.Timestamp.freeze_to (time_block.start_time); session_manager.current_time_block = time_block; Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance (); assert_true (time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_true (session_manager.current_session == session); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, cycles + 1U ); } /** * Skipping completed long break should start new session. */ public void test_advance__completed_long_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var time_block = session_manager.current_session.get_last_time_block (); Ft.Timestamp.freeze_to (time_block.start_time); session_manager.current_time_block = time_block; Ft.Timestamp.advance (12 * Ft.Interval.MINUTE); session_manager.advance (); assert_true (time_block.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_true (session_manager.current_session != session); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); } /** * When has-uniform-breaks is true, expect POMODORO advance to a BREAK. */ public void test_advance__uniform_breaks () { var settings = Ft.get_settings (); settings.set_uint ("cycles", 1); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); // Start timer var now = Ft.Timestamp.peek (); timer.start (now); var session = session_manager.current_session; var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); assert_true (session_manager.has_uniform_breaks); assert_true (time_block_1.state == Ft.State.POMODORO); assert_true (time_block_2.state == Ft.State.BREAK); // Advance to a break once pomodoro finishes session_manager.advance (time_block_1.end_time); assert_true (session_manager.current_session == session); assert_false (session_manager.current_time_block == time_block_1); assert_true (session_manager.current_time_block == time_block_2); assert_true (time_block_1.state == Ft.State.POMODORO); assert_true (time_block_2.state == Ft.State.BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.duration), new GLib.Variant.int64 (session_manager.scheduler.session_template.short_break_duration) ); } public void test_advance_to_state__pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (1); // Short break var session_changed_emitted = 0U; var signals = new string[0]; session_manager.current_time_block = time_block_1; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); var now = time_block_1.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); session_manager.advance_to_state (Ft.State.POMODORO, now); assert_true (session_manager.current_session == session); var time_block_2 = session_manager.current_time_block; assert_true (time_block_2.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.get_completion_time ()), new GLib.Variant.int64 (session_manager.scheduler.calculate_time_block_completion_time (time_block_2)) ); assert_true (time_block_1.session == session); assert_true (time_block_2.session == session); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_true (time_block_2.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_true (timer.is_started ()); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "enter-time-block", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1U); } public void test_advance_to_state__short_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (0); var now = Ft.Timestamp.peek (); var session_changed_emitted = 0; var signals = new string[0]; // Start a Ft. session_manager.current_time_block = time_block_1; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Switch to a short-break. now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance_to_state (Ft.State.BREAK, now); var time_block_2 = session.get_next_time_block (time_block_1); assert_true (time_block_2.state == Ft.State.SHORT_BREAK); assert_true (session_manager.current_session == session); assert_true (session_manager.current_time_block == time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.get_completion_time ()), new GLib.Variant.int64 (session_manager.scheduler.calculate_time_block_completion_time (time_block_2)) ); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_true (time_block_2.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_true (timer.is_started ()); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "enter-time-block", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); var cycle = session_manager.get_current_cycle (); assert_cmpfloat (cycle.calculate_progress (now), GLib.CompareOperator.EQ, 0.0); } public void test_advance_to_state__stopped () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_first_time_block (); session_manager.current_time_block = time_block_1; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance_to_state (Ft.State.STOPPED, now); assert_true (session_manager.current_session == session); assert_null (session_manager.current_time_block); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (now) ); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); var time_block_2 = session.get_next_time_block (time_block_1); assert_true (time_block_2.get_status () == Ft.TimeBlockStatus.SCHEDULED); assert_true (time_block_2.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (now) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_false (timer.is_started ()); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); } /** * Advance to pomodoro during ongoing Ft. Expect the time-block to be extended. */ public void test_advance_to_state__extend_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.POMODORO); var session = session_manager.current_session; var time_block = session_manager.current_time_block; var expected_start_time = time_block.start_time; var expected_remaining_time = time_block.duration; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance_to_state (Ft.State.POMODORO, now); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_null (time_block.get_last_gap ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (expected_remaining_time) ); assert_cmpstrv (signals, { "resolve-state", "session-rescheduled", "state-changed", }); // Extend once more. Expect it to increase cycle weight. now = time_block.end_time - 10 * Ft.Interval.SECOND; Ft.Timestamp.freeze_to (now); session_manager.advance_to_state (Ft.State.POMODORO, now); assert_null (time_block.get_last_gap ()); assert_cmpfloat ( session_manager.get_current_cycle ().get_weight (), GLib.CompareOperator.EQ, 2.0 ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles - 1U ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (expected_remaining_time) ); } public void test_advance_to_state__extend_short_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.SHORT_BREAK); var session = session_manager.current_session; var time_block = session_manager.current_time_block; var expected_start_time = time_block.start_time; var expected_remaining_time = time_block.duration; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); // Extend the long break using .advance() var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance_to_state (Ft.State.SHORT_BREAK, now); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.state == Ft.State.SHORT_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_null (time_block.get_last_gap ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (expected_remaining_time) ); assert_cmpfloat_with_epsilon ( timer.calculate_progress (now), 1.0 / (1.0 + 5.0), 0.001 ); assert_cmpstrv (signals, { "resolve-state", "session-rescheduled", "state-changed", }); } public void test_advance_to_state__extend_long_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); // Create a session and mark all time blocks except the long break as completed. session_manager.ensure_session (); var session = session_manager.current_session; session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); // Start the long break. var time_block = session.get_last_time_block (); assert_true (time_block.state == Ft.State.LONG_BREAK); Ft.Timestamp.freeze_to (time_block.start_time); session_manager.current_time_block = time_block; var expected_start_time = time_block.start_time; var expected_remaining_time = time_block.duration; var expected_cycles = this.session_template.cycles; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); // Extend the long break using .advance() var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance_to_state (Ft.State.LONG_BREAK, now); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.state == Ft.State.LONG_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_null (time_block.get_last_gap ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, expected_cycles ); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (expected_remaining_time) ); assert_cmpfloat_with_epsilon ( timer.calculate_progress (now), 1.0 / (1.0 + 15.0), 0.001 ); assert_cmpstrv (signals, { "resolve-state", "state-changed", }); } public void test_advance_to_state__switch_breaks () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var time_block_1 = session.get_nth_time_block (0); // Pomodoro var time_block_2 = session.get_nth_time_block (1); // Short break time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); // Start short break Ft.Timestamp.freeze_to (time_block_2.start_time); session_manager.current_time_block = time_block_2; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); timer.state_changed.connect (() => { assert_true (session_manager.current_state == session_manager.current_time_block.state); }); // Switch to a long break after a minute var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); var expected_start_time = time_block_2.start_time; session_manager.advance_to_state (Ft.State.LONG_BREAK, now); assert_true (session_manager.current_time_block.state == Ft.State.LONG_BREAK); assert_true (session_manager.current_time_block == time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (this.session_template.long_break_duration) ); assert_cmpfloat_with_epsilon ( timer.calculate_progress (now), 1.0 / (1.0 + 15.0), 0.001 ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, 1U ); assert_cmpstrv (signals, { "resolve-state", "state-changed", "session-rescheduled" }); // Switch back to a short break. now = Ft.Timestamp.advance (Ft.Interval.MINUTE); signals = {}; session_manager.advance_to_state (Ft.State.SHORT_BREAK, now); assert_true (session_manager.current_time_block.state == Ft.State.SHORT_BREAK); assert_true (session_manager.current_time_block == time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (this.session_template.short_break_duration) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, 4U ); assert_cmpstrv (signals, { "resolve-state", "state-changed", "session-rescheduled" }); } /** * Manually advance to the same state as the upcoming time-block */ public void test_advance_to_state__confirm_advancement () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); settings.set_boolean ("confirm-starting-pomodoro", true); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.POMODORO); var session = session_manager.current_session; var pomodoro = session_manager.current_time_block; var short_break = session.get_next_time_block (pomodoro); Ft.Timestamp.freeze_to (pomodoro.end_time); timer.finish (pomodoro.end_time); // Confirm after 1 minute. var now = pomodoro.end_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); session_manager.advance_to_state (Ft.State.SHORT_BREAK, now); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.duration), new GLib.Variant.int64 (26 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.end_time), new GLib.Variant.int64 (now) ); assert_true (session_manager.current_time_block == short_break); assert_cmpvariant ( new GLib.Variant.int64 (short_break.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE) ); assert_cmpfloat ( timer.calculate_progress (now), GLib.CompareOperator.EQ, 0.0 ); } /** * Manually extend current state instead of confirming advancement */ public void test_advance_to_state__skip_advancement () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); settings.set_boolean ("confirm-starting-pomodoro", true); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.POMODORO); var pomodoro = session_manager.current_time_block; var expected_start_time = pomodoro.start_time; Ft.Timestamp.freeze_to (pomodoro.end_time); timer.finish (pomodoro.end_time); // Advance to a POMODORO after 1 minute. var now = pomodoro.end_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); session_manager.advance_to_state (Ft.State.POMODORO, now); assert_true (session_manager.current_time_block == pomodoro); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon ( timer.calculate_progress (now), (25.0 + 1.0) / (25.0 + 1.0 + 25.0), 0.001 ); } /** * Skipping to a pomodoro when the session is completed should start a new session. */ public void test_advance_to_state__completed_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break = session.get_last_time_block (); Ft.Timestamp.freeze_to (long_break.start_time); session_manager.current_time_block = long_break; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var now = long_break.get_completion_time (); Ft.Timestamp.freeze_to (now); session_manager.advance (now); var next_session = session_manager.current_session; var next_time_block = session_manager.current_time_block; assert_true (next_session != session); assert_nonnull (next_session); assert_nonnull (next_time_block); assert_true (next_time_block.state == Ft.State.POMODORO); assert_true (next_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (next_time_block.start_time), new GLib.Variant.int64 (now) ); assert_true (timer.is_started ()); assert_cmpuint ( next_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "leave-session", "session-rescheduled", "enter-session", "enter-time-block", "resolve-state", "state-changed" }); } /** * Skipping to a pomodoro when a long break hasn't been completed should add extra cycle. */ public void test_advance_to_state__uncompleted_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var long_break = session.get_last_time_block (); Ft.Timestamp.freeze_to (long_break.start_time); session_manager.current_time_block = long_break; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var now = long_break.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); session_manager.advance_to_state (Ft.State.POMODORO, now); assert_true (session_manager.current_session == session); var pomodoro = session_manager.current_time_block; assert_nonnull (pomodoro); assert_true (pomodoro.state == Ft.State.POMODORO); assert_true (pomodoro.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (long_break.end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.start_time), new GLib.Variant.int64 (now) ); assert_true (timer.is_started ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "enter-time-block", "resolve-state", "state-changed" }); } public void test_confirm_starting_break () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); // Confirm after 1 minute. session_manager.advance_to_state (Ft.State.POMODORO); var session = session_manager.current_session; var time_block_1 = session_manager.current_time_block; time_block_1.notify["end-time"].connect ( () => { assert_true ( time_block_1.get_status () == Ft.TimeBlockStatus.IN_PROGRESS ); }); var finished_time_1 = time_block_1.end_time; Ft.Timestamp.freeze_to (finished_time_1); timer.finish (finished_time_1); assert_true (timer.user_data == time_block_1); assert_true (timer.is_finished ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.finished_time), new GLib.Variant.int64 (finished_time_1) ); var confirmation_time_1 = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance (confirmation_time_1); var time_block_2 = session_manager.current_time_block; assert_true (timer.user_data == time_block_2); assert_true (timer.is_started ()); assert_false (timer.is_finished ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (confirmation_time_1) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.offset), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (confirmation_time_1) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (confirmation_time_1) ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, 4U ); // Confirm after 30 minutes. session_manager.advance_to_state (Ft.State.POMODORO); var time_block_3 = session_manager.current_time_block; time_block_3.notify["end-time"].connect ( () => { assert_true ( time_block_3.get_status () == Ft.TimeBlockStatus.IN_PROGRESS ); }); var finished_time_2 = time_block_3.end_time; Ft.Timestamp.freeze_to (finished_time_2); timer.finish (finished_time_2); assert_true (timer.user_data == time_block_3); assert_true (timer.is_finished ()); var confirmation_time_2 = Ft.Timestamp.advance (30 * Ft.Interval.MINUTE); session_manager.advance (confirmation_time_2); var time_block_4 = session_manager.current_time_block; assert_true (timer.user_data == time_block_4); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (confirmation_time_2) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.offset), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_3.end_time), new GLib.Variant.int64 (confirmation_time_2) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_4.start_time), new GLib.Variant.int64 (confirmation_time_2) ); // Because we extended previous pomodoro, expect one less cycle assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, 3U ); } public void test_confirm_starting_pomodoro () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-pomodoro", true); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.SHORT_BREAK); var time_block_1 = session_manager.current_time_block; time_block_1.notify["end-time"].connect ( () => { assert_true ( time_block_1.get_status () == Ft.TimeBlockStatus.IN_PROGRESS ); }); var finished_time = session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (finished_time); timer.finish (finished_time); assert_true (timer.user_data == time_block_1); assert_true (timer.is_finished ()); // Confirm after 1 minute. var confirmation_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_manager.advance (confirmation_time); var time_block_2 = session_manager.current_time_block; assert_true (time_block_2.state == Ft.State.POMODORO); assert_true (timer.user_data == time_block_2); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (confirmation_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.offset), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (confirmation_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (confirmation_time) ); } /* * Tests for handling session expiry */ public void test_reset__empty_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); session_manager.current_session = session; // Reset should not do anything if session is scheduled / already empty. session_manager.reset (); assert_true (session_manager.current_session == session); } public void test_reset () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var session_changed_emitted = 0; var notify_expiry_time_emitted = 0; var signals = new string[0]; session_manager.current_time_block = session.get_first_time_block (); timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); session.notify["expiry-time"].connect (() => { notify_expiry_time_emitted++; }); var timestamp = Ft.Timestamp.peek (); session_manager.reset (timestamp); assert_true (session_manager.current_session != session); assert_null (session_manager.current_time_block); assert_cmpvariant ( new GLib.Variant.int64 (session.expiry_time), new GLib.Variant.int64 (timestamp) ); assert_nonnull (session_manager.current_session); assert_true (session_manager.current_session.is_scheduled ()); assert_cmpuint ( session_manager.current_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_session.expiry_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpstrv (signals, { "leave-time-block", "leave-session", "session-rescheduled", "enter-session", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (notify_expiry_time_emitted, GLib.CompareOperator.EQ, 1); } /** * Check whether session expires after a timeout. * * In this test we manually set session `expiry-time`, which doesn't happen normally. */ public void test_expire_session__after_timeout () { Ft.Timestamp.thaw (); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var main_context = GLib.MainContext.@default (); session_manager.current_session = session; session_manager.current_time_block = session.get_first_time_block (); var session_expired_emitted = 0; session_manager.session_expired.connect (() => { session_expired_emitted++; }); var leave_session_emitted = 0; session_manager.leave_session.connect (() => { leave_session_emitted++; }); var enter_session_emitted = 0; session_manager.enter_session.connect (() => { enter_session_emitted++; }); var now = Ft.Timestamp.from_now (); var elapsed = 0U; var timeout_id = GLib.Timeout.add (100, () => { elapsed += 100; return GLib.Source.CONTINUE; }); session.expiry_time = now + 500 * Ft.Interval.MILLISECOND; while (elapsed < 2000) { main_context.iteration (true); if (session_expired_emitted > 0) { break; } } GLib.Source.remove (timeout_id); assert_cmpuint (session_expired_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (leave_session_emitted, GLib.CompareOperator.EQ, 1); assert_false (timer.is_started ()); // Expect to a next session to be initialized. assert_cmpuint (enter_session_emitted, GLib.CompareOperator.EQ, 1); assert_nonnull (session_manager.current_session); assert_true (session_manager.current_session.is_scheduled ()); } /** * Check whether session expires after after a mock system suspend. */ public void test_expire_session__after_suspend () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.reset (now); assert_false (session_manager.current_session.is_scheduled ()); var suspend_start = now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.suspending (suspend_start); var suspend_end = Ft.Timestamp.advance (Ft.SessionManager.SESSION_EXPIRY_TIMEOUT); timer.suspended (suspend_start, suspend_end); // Expect to a next session to be initialized. assert_nonnull (session_manager.current_session); assert_true (session_manager.current_session.is_scheduled ()); } public void test_settings_change () { var session_manager = new Ft.SessionManager (); var signals = new string[0]; var sesssion = this.create_session (session_manager); session_manager.current_session = sesssion; session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 10); settings.set_uint ("short-break-duration", 20); settings.set_uint ("long-break-duration", 30); settings.set_uint ("cycles", 3); assert_false (session_manager.has_uniform_breaks); // Expect reschedule to be applied at idle. assert_cmpstrv (signals, {}); Ft.Timestamp.advance (Ft.Interval.MINUTE); var main_context = GLib.MainContext.@default (); while (main_context.iteration (false)); assert_cmpvariant ( session_manager.scheduler.session_template.to_variant (), Ft.SessionTemplate.with_defaults ().to_variant () ); assert_cmpstrv (signals, { "session-rescheduled", }); // Ensure reschedule done its job. var time_block_1 = sesssion.get_nth_time_block (0); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.get_intended_duration ()), new GLib.Variant.int64 (10 * Ft.Interval.SECOND) ); var time_block_2 = sesssion.get_nth_time_block (1); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.get_intended_duration ()), new GLib.Variant.int64 (20 * Ft.Interval.SECOND) ); // Check whether has-uniform-breaks gets updated settings.set_uint ("cycles", 1); assert_true (session_manager.has_uniform_breaks); } } /* * Tests for SessionManager, but with calls invoked by timer. */ public class SessionManagerTimerTest : Tests.TestSuite { private Ft.Timer timer; private Ft.SessionTemplate session_template = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 4 }; public SessionManagerTimerTest () { // this.add_test ("timer_set_state", this.test_timer_set_state); this.add_test ("timer_set_duration", this.test_timer_set_duration); this.add_test ("timer_start__initialize_session", this.test_timer_start__initialize_session); this.add_test ("timer_start__ignore_call", this.test_timer_start__ignore_call); this.add_test ("timer_start__continue_session", this.test_timer_start__continue_session); this.add_test ("timer_start__expire_session", this.test_timer_start__expire_session); this.add_test ("timer_reset__mark_as_uncompleted", this.test_timer_reset__mark_as_uncompleted); this.add_test ("timer_reset__mark_as_completed", this.test_timer_reset__mark_as_completed); this.add_test ("timer_reset__paused", this.test_timer_reset__paused); this.add_test ("timer_reset__uncompleted_pomodoro", this.test_timer_reset__uncompleted_pomodoro); this.add_test ("timer_reset__completed_pomodoro", this.test_timer_reset__completed_pomodoro); this.add_test ("timer_reset__uncompleted_last_pomodoro", this.test_timer_reset__uncompleted_last_pomodoro); this.add_test ("timer_reset__completed_last_pomodoro", this.test_timer_reset__completed_last_pomodoro); this.add_test ("timer_reset__uncompleted_long_break", this.test_timer_reset__uncompleted_long_break); this.add_test ("timer_reset__completed_long_break", this.test_timer_reset__completed_long_break); this.add_test ("timer_reset__uncompleted_extra_pomodoro", this.test_timer_reset__uncompleted_extra_pomodoro); this.add_test ("timer_reset__completed_extra_pomodoro", this.test_timer_reset__completed_extra_pomodoro); this.add_test ("timer_reset__ignore_call", this.test_timer_reset__ignore_call); this.add_test ("timer_pause", this.test_timer_pause); this.add_test ("timer_pause__mark_interruption", this.test_timer_pause__mark_interruption); this.add_test ("timer_pause__unmark_interruption", this.test_timer_pause__unmark_interruption); this.add_test ("timer_resume", this.test_timer_resume); this.add_test ("timer_rewind", this.test_timer_rewind); this.add_test ("timer_rewind__multiple", this.test_timer_rewind__multiple); this.add_test ("timer_rewind__paused", this.test_timer_rewind__paused); this.add_test ("timer_rewind__paused_after_completion", this.test_timer_rewind__paused_after_completion); this.add_test ("timer_rewind__paused_multiple", this.test_timer_rewind__paused_multiple); this.add_test ("timer_finished__continuous", this.test_timer_finished__continuous); this.add_test ("timer_finished__wait_for_activity", this.test_timer_finished__wait_for_activity); this.add_test ("timer_finished__manual", this.test_timer_finished__manual); // this.add_test ("timer_suspended", this.test_timer_suspended); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); } public override void teardown () { var settings = Ft.get_settings (); settings.revert (); Ft.Timer.set_default (null); } // public void test_timer_set_state () // { // } public void test_timer_set_duration () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; var now = Ft.Timestamp.peek (); timer.start (now); var session = session_manager.current_session; var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); var initial_duration = time_block_1.duration; var expected_duration = timer.duration + Ft.Interval.MINUTE; var expected_elapsed = timer.calculate_elapsed (now); timer.duration = expected_duration; assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (expected_elapsed) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (expected_duration) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.duration), new GLib.Variant.int64 (expected_duration) ); assert_cmpvariant ( time_block_1.get_intended_duration (), new GLib.Variant.int64 (initial_duration) ); assert_true (session_manager.current_time_block == time_block_1); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpstrv (signals, { "resolve-state", "session-rescheduled", "state-changed" }); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (time_block_1.start_time + expected_duration) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (time_block_1.end_time) ); } /** * Check timer.start() call for a timer managed by session manager. * * Expect session manager to resolve timer state into a POMODORO time-block. */ public void test_timer_start__initialize_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session_template = session_manager.scheduler.session_template; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); assert_null (session_manager.current_session); assert_false (timer.is_started ()); assert_false (timer.is_running ()); var now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.start (now); assert_nonnull (session_manager.current_session); assert_cmpuint ( session_manager.current_session.get_cycles ().length (), GLib.CompareOperator.EQ, session_template.cycles ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_session.start_time), new GLib.Variant.int64 (timer.state.started_time) ); assert_nonnull (session_manager.current_time_block); assert_true (session_manager.current_time_block == session_manager.current_session.get_first_time_block ()); assert_true (session_manager.current_time_block.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_time_block.start_time), new GLib.Variant.int64 (timer.state.started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_time_block.start_time), new GLib.Variant.int64 (now) ); assert_cmpfloat_with_epsilon (session_manager.current_time_block.get_weight (), 1.0, EPSILON); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (session_template.pomodoro_duration) ); assert_true (timer.user_data == session_manager.current_time_block); assert_true (timer.is_started ()); assert_true (timer.is_running ()); assert_cmpstrv (signals, { "session-rescheduled", "enter-session", "enter-time-block", "resolve-state", "state-changed" }); } /** * Call timer.start() while timer is already running. Expect call to be ignored. */ public void test_timer_start__ignore_call () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var expected_time_block = session_manager.current_time_block; var expected_state = timer.state.copy (); var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.start (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (session_manager.current_time_block == expected_time_block); assert_cmpstrv (signals, {}); } /** * Expect to start a new pomodoro when starting the timer again. */ public void test_timer_start__continue_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); var time_block_1 = session_manager.current_session.get_nth_time_block (0); var session = time_block_1.session; var signals = new string[0]; // Stop the timer now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.reset (now); assert_nonnull (session_manager.current_session); assert_null (session_manager.current_time_block); assert_true (time_block_1.state == Ft.State.POMODORO); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); // Start the timer again timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.start (now); var time_block_2 = session_manager.current_time_block; assert_true (time_block_2.state == Ft.State.POMODORO); assert_true (session.get_next_time_block (time_block_1) == time_block_2); assert_true (time_block_1.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_true (time_block_2.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpstrv (signals, { "session-rescheduled", "enter-time-block", "resolve-state", "state-changed" }); } /** * Start timer after 1h from last time-block. Expect previous session to expire. */ public void test_timer_start__expire_session () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); var state_changed_emitted = 0; timer.start (now); assert_nonnull (session_manager.current_session); assert_nonnull (session_manager.current_time_block); assert_true (timer.is_started ()); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); var reset_time = now; timer.reset (now); assert_nonnull (session_manager.current_session); assert_null (session_manager.current_time_block); assert_false (timer.is_started ()); now = Ft.Timestamp.advance (Ft.SessionManager.SESSION_EXPIRY_TIMEOUT); var expired_session = session_manager.current_session; expired_session.changed.connect (() => { state_changed_emitted++; }); assert_true (expired_session.is_expired (now)); timer.start (now); assert_false (session_manager.current_session == expired_session); assert_nonnull (session_manager.current_time_block); assert_true (timer.is_started ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (expired_session.end_time), new GLib.Variant.int64 (reset_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_session.start_time), new GLib.Variant.int64 (now) ); } public void test_timer_reset__mark_as_uncompleted () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); var time_block = session_manager.current_time_block; var session = session_manager.current_session; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.reset (now); assert_null (session_manager.current_time_block); assert_nonnull (session_manager.current_session); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint ( session_manager.current_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed", }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } public void test_timer_reset__mark_as_completed () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); var time_block = session_manager.current_time_block; var session = session_manager.current_session; var session_changed_emitted = 0; var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); now = time_block.end_time - Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_cmpuint ( session_manager.current_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed", }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Ensure that pausing the timer will not affect whether time-block will be marked as completed. */ public void test_timer_reset__paused () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); var time_block = session_manager.current_time_block; var completion_time = time_block.get_completion_time (); now = completion_time - Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.pause (now); var signals = new string[0]; var gaps_count = 0U; var gap_start_time = Ft.Timestamp.UNDEFINED; var gap_end_time = Ft.Timestamp.UNDEFINED; var expected_gap_start_time = now; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); now = completion_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_cmpuint ( session_manager.current_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (gap_start_time), new GLib.Variant.int64 (expected_gap_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_end_time), new GLib.Variant.int64 (now) ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed", }); } /** * Stopping a uncompleted Ft. * * Expect a pomodoro to be scheduled next and the number of visible cycles to not change. */ public void test_timer_reset__uncompleted_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var session = session_manager.current_session; var pomodoro = session_manager.current_time_block; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer before pomodoro is completed var now = pomodoro.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_true (pomodoro.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.end_time), new GLib.Variant.int64 (now) ); // After stopping pomodoro, another pomodoro should be scheduled var scheduled_pomodoro = session.get_next_time_block (pomodoro); assert_nonnull (scheduled_pomodoro); assert_true (scheduled_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Visible cycles should remain the same assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stopping a completed Ft. * * Expect a pomodoro to be scheduled next and the number of visible cycles to not change. */ public void test_timer_reset__completed_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var session = session_manager.current_session; var pomodoro = session_manager.current_time_block; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer after pomodoro is completed var now = pomodoro.get_completion_time () + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_true (pomodoro.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (pomodoro.end_time), new GLib.Variant.int64 (now) ); // After stopping pomodoro, another pomodoro should be scheduled var scheduled_pomodoro = session.get_next_time_block (pomodoro); assert_nonnull (scheduled_pomodoro); assert_true (scheduled_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Visible cycles should remain the same assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stopping last pomodoro should schedule another cycle. * * Expect a pomodoro to be scheduled next and the number of visible cycles to not change. */ public void test_timer_reset__uncompleted_last_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var long_break = session.get_last_time_block (); assert_true (long_break.state == Ft.State.LONG_BREAK); var last_pomodoro = session.get_previous_time_block (long_break); assert_true (last_pomodoro.state == Ft.State.POMODORO); session.@foreach ( (time_block) => { if (time_block != last_pomodoro && time_block != long_break) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); Ft.Timestamp.freeze_to (last_pomodoro.start_time); session_manager.current_time_block = last_pomodoro; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer before pomodoro is completed var now = last_pomodoro.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_true (last_pomodoro.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (last_pomodoro.end_time), new GLib.Variant.int64 (now) ); // After stopping pomodoro, another pomodoro should be scheduled var scheduled_pomodoro = session.get_next_time_block (last_pomodoro); assert_nonnull (scheduled_pomodoro); assert_true (scheduled_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Visible cycles should remain the same assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stopping a completed last pomodoro should schedule extra cycle. * * When the last pomodoro (the one before long break) is completed and then stopped, * an extra cycle should be scheduled so that the user can take a long break later. */ public void test_timer_reset__completed_last_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var long_break = session.get_last_time_block (); assert_true (long_break.state == Ft.State.LONG_BREAK); var last_pomodoro = session.get_previous_time_block (long_break); assert_true (last_pomodoro.state == Ft.State.POMODORO); session.@foreach ( (time_block) => { if (time_block != last_pomodoro && time_block != long_break) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); Ft.Timestamp.freeze_to (last_pomodoro.start_time); session_manager.current_time_block = last_pomodoro; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer after pomodoro is completed var now = last_pomodoro.get_completion_time () + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_null (session_manager.current_time_block); assert_true (last_pomodoro.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (last_pomodoro.end_time), new GLib.Variant.int64 (now) ); // After stopping pomodoro, another pomodoro should be scheduled. // Session gets completed only after completing a long-break. var scheduled_pomodoro = session.get_next_time_block (last_pomodoro); assert_nonnull (scheduled_pomodoro); assert_true (scheduled_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Visible cycles should increase by one assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stop a session with completed all cycles, without completing a long-break. * * Expect an extra cycle. */ public void test_timer_reset__uncompleted_long_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var session = new Ft.Session.from_template (this.session_template); var long_break = session.get_last_time_block (); session.@foreach ( (time_block) => { time_block.set_completion_time (time_block.end_time); if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); session_manager.scheduler.ensure_session_meta (session); Ft.Timestamp.freeze_to (long_break.start_time); session_manager.current_time_block = long_break; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); var now = long_break.start_time + Ft.Interval.MINUTE; timer.reset (now); assert_true (long_break.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Hit stop after completing a long-break. * * Expect a new session to be initialized. */ public void test_timer_reset__completed_long_break () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.ensure_session (); var session = session_manager.current_session; var long_break = session.get_last_time_block (); session.@foreach ( (time_block) => { time_block.set_status (time_block.state != Ft.State.LONG_BREAK ? Ft.TimeBlockStatus.COMPLETED : Ft.TimeBlockStatus.IN_PROGRESS); } ); Ft.Timestamp.freeze_to (long_break.start_time); session_manager.current_time_block = long_break; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); var now = long_break.get_completion_time () + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_true (session_manager.current_session != session); assert_nonnull (session_manager.current_session); assert_cmpuint ( session_manager.current_session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpstrv (signals, { "leave-time-block", "leave-session", "session-rescheduled", "enter-session", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stopping an uncompleted extra Ft. * * Expect a pomodoro to be scheduled next and the number of visible cycles to not change - * remain one extra above template. */ public void test_timer_reset__uncompleted_extra_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var scheduler = session_manager.scheduler; session_manager.ensure_session (); var session = session_manager.current_session; session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var uncompleted_long_break = session.get_last_time_block (); uncompleted_long_break.duration = Ft.Interval.MINUTE; uncompleted_long_break.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var extra_pomodoro = new Ft.TimeBlock (Ft.State.POMODORO); extra_pomodoro.set_time_range (session.end_time, session.end_time + 25 * Ft.Interval.MINUTE); extra_pomodoro.set_status (Ft.TimeBlockStatus.IN_PROGRESS); session.append (extra_pomodoro); var long_break = new Ft.TimeBlock (Ft.State.LONG_BREAK); long_break.set_time_range (session.end_time, session.end_time + 15 * Ft.Interval.MINUTE); long_break.set_status (Ft.TimeBlockStatus.SCHEDULED); session.append (long_break); scheduler.ensure_session_meta (session); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); var extra_cycle = session.get_cycles ().nth_data (4U); assert_true (extra_cycle.is_visible ()); Ft.Timestamp.freeze_to (extra_pomodoro.start_time); session_manager.current_time_block = extra_pomodoro; assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer before extra pomodoro got completed var now = extra_pomodoro.start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_cmpvariant ( new GLib.Variant.int64 (extra_pomodoro.end_time), new GLib.Variant.int64 (now) ); assert_true (extra_pomodoro.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpfloat ( extra_pomodoro.get_weight (), GLib.CompareOperator.EQ, 0.0 ); assert_false (extra_cycle.is_visible ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); var scheduled_extra_pomodoro = session.get_next_time_block (extra_pomodoro); assert_true (scheduled_extra_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_extra_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Stopping a completed extra pomodoro * * When stopping a pomodoro always expect a pomodoro to be scheduled next. */ public void test_timer_reset__completed_extra_pomodoro () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var scheduler = session_manager.scheduler; session_manager.ensure_session (); var session = session_manager.current_session; session.@foreach ( (time_block) => { if (time_block.state != Ft.State.LONG_BREAK) { time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } } ); var uncompleted_long_break = session.get_last_time_block (); uncompleted_long_break.duration = Ft.Interval.MINUTE; uncompleted_long_break.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var extra_pomodoro = new Ft.TimeBlock (Ft.State.POMODORO); extra_pomodoro.set_time_range (session.end_time, session.end_time + 25 * Ft.Interval.MINUTE); extra_pomodoro.set_status (Ft.TimeBlockStatus.IN_PROGRESS); session.append (extra_pomodoro); var long_break = new Ft.TimeBlock (Ft.State.LONG_BREAK); long_break.set_time_range (session.end_time, session.end_time + 15 * Ft.Interval.MINUTE); long_break.set_status (Ft.TimeBlockStatus.SCHEDULED); session.append (long_break); scheduler.ensure_session_meta (session); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 1U ); var extra_cycle = session.get_cycles ().nth_data (4U); assert_true (extra_cycle.is_visible ()); Ft.Timestamp.freeze_to (extra_pomodoro.start_time); session_manager.current_time_block = extra_pomodoro; var signals = new string[0]; var session_changed_emitted = 0; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); // Stop the timer after extra pomodoro is completed var completion_time = extra_pomodoro.get_completion_time (); var now = completion_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (now); timer.reset (now); assert_cmpvariant ( new GLib.Variant.int64 (extra_pomodoro.end_time), new GLib.Variant.int64 (now) ); assert_true (extra_pomodoro.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_true (extra_cycle.is_visible ()); // After stopping completed extra pomodoro, another extra pomodoro should be scheduled var scheduled_extra_pomodoro = session.get_next_time_block (extra_pomodoro); assert_nonnull (scheduled_extra_pomodoro); assert_true (scheduled_extra_pomodoro.state == Ft.State.POMODORO); assert_true (scheduled_extra_pomodoro.get_status () == Ft.TimeBlockStatus.SCHEDULED); // Visible cycles should increase by one (another extra cycle added) assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles + 2U ); assert_cmpstrv (signals, { "leave-time-block", "session-rescheduled", "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Timer.reset() should be ignored when there is no current-time-block. */ public void test_timer_reset__ignore_call () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); Ft.Timestamp.advance (Ft.Interval.MINUTE); timer.reset (); assert_null (session_manager.current_session); assert_null (session_manager.current_time_block); assert_cmpstrv (signals, {}); } public void test_timer_pause () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var started_time = Ft.Timestamp.peek (); timer.start (started_time); var session = session_manager.current_session; var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); var session_changed_emitted = 0U; session.changed.connect (() => { session_changed_emitted++; }); var paused_time = started_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (paused_time); var expected_cycle_progress = cycle.calculate_progress (paused_time); timer.pause (paused_time); assert_true (timer.is_paused ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.paused_time), new GLib.Variant.int64 (paused_time) ); assert_nonnull (session_manager.current_gap); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_gap.start_time), new GLib.Variant.int64 (paused_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_gap.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (paused_time)), new GLib.Variant.int64 (Ft.Interval.MINUTE) ); assert_true (cycle.is_visible ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpfloat_with_epsilon ( cycle.calculate_progress (paused_time), expected_cycle_progress, EPSILON ); assert_cmpstrv (signals, { "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1U); // Pause after the completion_time. Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.resume (); signals = {}; session_changed_emitted = 0U; var completion_time = time_block.get_completion_time (); Ft.Timestamp.freeze_to (completion_time); timer.pause (completion_time); assert_true (timer.is_paused ()); assert_nonnull (session_manager.current_gap); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_gap.start_time), new GLib.Variant.int64 (completion_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_gap.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_true (cycle.is_visible ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (completion_time), 1.0, EPSILON); assert_cmpvariant ( new GLib.Variant.int64 (session.expiry_time), new GLib.Variant.int64 (completion_time + Ft.SessionManager.SESSION_EXPIRY_TIMEOUT) ); assert_cmpstrv (signals, { "resolve-state", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1U); } /** * Pausing the timer during a pomodoro should mark the created gap as an INTERRUPTION. */ public void test_timer_pause__mark_interruption () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var timer_action_group = new Ft.TimerActionGroup.with_timer (timer); var now = Ft.Timestamp.peek (); timer_action_group.activate_action ("start", null); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer_action_group.activate_action ("pause", null); var time_block = session_manager.current_time_block; var gap = time_block.get_last_gap (); assert_nonnull (gap); assert_true (gap.has_flag (Ft.GapFlags.INTERRUPTION)); assert_true (Ft.Timestamp.is_defined (gap.start_time)); assert_false (Ft.Timestamp.is_defined (gap.end_time)); // Resume the timer; expect INTERRUPTION to be preserved now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer_action_group.activate_action ("resume", null); gap = time_block.get_last_gap (); assert_true (gap.has_flag (Ft.GapFlags.INTERRUPTION)); assert_true (Ft.Timestamp.is_defined (gap.end_time)); } /** * Pausing then stopping should invalidate the INTERRUPTION by changing gap type to OTHER. */ public void test_timer_pause__unmark_interruption () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var timer_action_group = new Ft.TimerActionGroup.with_timer (timer); var now = Ft.Timestamp.peek (); timer_action_group.activate_action ("start", null); now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer_action_group.activate_action ("pause", null); var time_block = session_manager.current_time_block; var gap = time_block.get_last_gap (); assert_nonnull (gap); assert_true (gap.has_flag (Ft.GapFlags.INTERRUPTION)); assert_true (Ft.Timestamp.is_defined (gap.start_time)); assert_false (Ft.Timestamp.is_defined (gap.end_time)); // Stop the timer; expect the interruption to be invalidated now = Ft.Timestamp.advance (Ft.Interval.MINUTE); timer_action_group.activate_action ("reset", null); assert_null (session_manager.current_time_block); assert_false (gap.has_flag (Ft.GapFlags.INTERRUPTION)); assert_true (Ft.Timestamp.is_defined (gap.end_time)); } public void test_timer_resume () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var start_time = Ft.Timestamp.peek (); timer.start (start_time); var session = session_manager.current_session; var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var pause_time = start_time + 3 * Ft.Interval.MINUTE; var expected_time_block_start_time = time_block.start_time; var expected_time_block_end_time = time_block.end_time + Ft.Interval.MINUTE; var expected_gap_start_time = pause_time; var expected_gap_end_time = pause_time + Ft.Interval.MINUTE; var expected_cycle_progress = cycle.calculate_progress (pause_time); timer.pause (pause_time); assert_true (timer.is_paused ()); var signals = new string[0]; var session_changed_emitted = 0U; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); session.changed.connect (() => { session_changed_emitted++; }); var resume_time = expected_gap_end_time; Ft.Timestamp.freeze_to (resume_time); timer.resume (resume_time); assert_false (timer.is_paused ()); assert_null (session_manager.current_gap); var gap = session_manager.current_time_block.get_last_gap (); assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (expected_gap_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (expected_gap_end_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (resume_time)), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_time_block_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (expected_time_block_end_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (session_manager.scheduler.calculate_time_block_completion_time (time_block)) ); assert_true (cycle.is_visible ()); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpfloat_with_epsilon ( cycle.calculate_progress (resume_time), expected_cycle_progress, EPSILON ); assert_cmpstrv (signals, { "resolve-state", "session-rescheduled", "state-changed" }); assert_cmpuint (session_changed_emitted, GLib.CompareOperator.EQ, 1U); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_session.expiry_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); } public void test_timer_rewind () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var start_time = Ft.Timestamp.peek (); timer.start (start_time); var rewind_time = start_time + 3 * Ft.Interval.MINUTE; var session = session_manager.current_session; var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var expected_time_block_start_time = time_block.start_time; var expected_time_block_end_time = time_block.end_time + Ft.Interval.MINUTE; var expected_time_block_completion_time = time_block.get_completion_time () + Ft.Interval.MINUTE; var expected_gap_end_time = rewind_time; var expected_gap_start_time = expected_gap_end_time - Ft.Interval.MINUTE; var expected_cycle_progress = cycle.calculate_progress (expected_gap_start_time); var signals = new string[0]; timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); session_manager.enter_session.connect (() => { signals += "enter-session"; }); session_manager.enter_time_block.connect (() => { signals += "enter-time-block"; }); session_manager.leave_session.connect (() => { signals += "leave-session"; }); session_manager.leave_time_block.connect (() => { signals += "leave-time-block"; }); session_manager.session_rescheduled.connect (() => { signals += "session-rescheduled"; }); timer.rewind (Ft.Interval.MINUTE, rewind_time); assert_false (timer.is_paused ()); assert_true (timer.is_running ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (rewind_time)), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (expected_time_block_completion_time) ); var gap = session_manager.current_time_block.get_last_gap (); assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (expected_gap_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (expected_gap_end_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_time_block_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (expected_time_block_end_time) ); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpfloat_with_epsilon ( cycle.calculate_progress (rewind_time), expected_cycle_progress, EPSILON ); assert_cmpuint ( session.count_visible_cycles (), GLib.CompareOperator.EQ, this.session_template.cycles ); assert_cmpvariant ( new GLib.Variant.int64 (session.expiry_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpstrv (signals, { "resolve-state", "session-rescheduled", "state-changed" }); } /** * Use rewind several times. * * When gaps overlay, expect them to be merged. */ public void test_timer_rewind__multiple () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var gaps_count = 0U; var gap_start_time = Ft.Timestamp.UNDEFINED; var gap_end_time = Ft.Timestamp.UNDEFINED; // First rewind. now = Ft.Timestamp.advance (3 * Ft.Interval.MINUTE); timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE) ); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (gap_start_time), new GLib.Variant.int64 (time_block.start_time + 2 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_end_time), new GLib.Variant.int64 (now) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 0.1, EPSILON); // Second rewind. gaps_count = 0; gap_start_time = Ft.Timestamp.UNDEFINED; gap_end_time = Ft.Timestamp.UNDEFINED; now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (65 * Ft.Interval.SECOND) ); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (gap_start_time), new GLib.Variant.int64 (time_block.start_time + 65 * Ft.Interval.SECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (time_block.end_time - 5 * Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 65.0 / 1200.0, EPSILON); // Third rewind. gaps_count = 0; gap_start_time = Ft.Timestamp.UNDEFINED; gap_end_time = Ft.Timestamp.UNDEFINED; now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (5 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (gap_start_time), new GLib.Variant.int64 (time_block.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_end_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (time_block.end_time - 5 * Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 0.0, EPSILON); } public void test_timer_rewind__paused () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); now = Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.pause (now); var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var gaps_count = 0U; var expected_completion_time = time_block.get_completion_time () + Ft.Interval.MINUTE; now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (Ft.Interval.MINUTE, now); assert_true (timer.is_paused ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (expected_completion_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_manager.current_session.expiry_time), new GLib.Variant.int64 (now + Ft.SessionManager.SESSION_EXPIRY_TIMEOUT) ); time_block.foreach_gap ((gap) => { gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 2); assert_cmpvariant ( new GLib.Variant.int64 (cycle.get_completion_time ()), new GLib.Variant.int64 (expected_completion_time) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (timer.state.paused_time), 4.0 / 20.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); } public void test_timer_rewind__paused_after_completion () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); now = Ft.Timestamp.advance (22 * Ft.Interval.MINUTE); timer.pause (now); var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var expected_completion_time = time_block.get_completion_time (); // First rewind. now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (Ft.Interval.MINUTE, now); assert_true (timer.is_paused ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (21 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (expected_completion_time) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 1.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); // Second rewind. timer.rewind (25 * Ft.Interval.MINUTE, now); assert_true (timer.is_paused ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 0.0 / 20.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); } public void test_timer_rewind__paused_multiple () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var now = Ft.Timestamp.peek (); timer.start (now); now = Ft.Timestamp.advance (3 * Ft.Interval.MINUTE); timer.pause (now); var time_block = session_manager.current_time_block; var cycle = session_manager.get_current_cycle (); var gaps_count = 0U; var gap_start_time = Ft.Timestamp.UNDEFINED; var gap_end_time = Ft.Timestamp.UNDEFINED; var original_completion_time = time_block.get_completion_time (); // First rewind. now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE) ); time_block.foreach_gap ((gap) => { gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (original_completion_time + Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (timer.state.paused_time), 2.0 / 20.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); // Second rewind. gaps_count = 0; gap_start_time = Ft.Timestamp.UNDEFINED; gap_end_time = Ft.Timestamp.UNDEFINED; now = Ft.Timestamp.advance (5 * Ft.Interval.SECOND); timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (Ft.Interval.MINUTE) ); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (original_completion_time + 2 * Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 1.0 / 20.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); // Third rewind. gaps_count = 0; gap_start_time = Ft.Timestamp.UNDEFINED; gap_end_time = Ft.Timestamp.UNDEFINED; now = Ft.Timestamp.advance (45 * Ft.Interval.MINUTE); timer.rewind (5 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_true (session_manager.current_time_block == time_block); assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); time_block.foreach_gap ((gap) => { gap_start_time = gap.start_time; gap_end_time = gap.end_time; gaps_count++; }); assert_cmpvariant ( new GLib.Variant.int64 (time_block.get_completion_time ()), new GLib.Variant.int64 (original_completion_time + 3 * Ft.Interval.MINUTE) ); assert_cmpfloat_with_epsilon (cycle.calculate_progress (now), 0.0, EPSILON); assert_cmpfloat_with_epsilon (cycle.get_weight (), 1.0, EPSILON); assert_true (cycle.is_visible ()); } public void test_timer_finished__continuous () { var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); timer.start (); var state_changed_call_count = 0; var finished_call_count = 0; var state_1 = Ft.TimerState (); var state_2 = Ft.TimerState (); timer.finished.connect (() => { finished_call_count++; }); timer.state_changed.connect ((current_state, previous_state) => { if (state_changed_call_count == 0) { state_1 = current_state; } if (state_changed_call_count == 1) { assert_true (previous_state.equals (state_1)); state_2 = current_state; } state_changed_call_count++; }); var finished_time = session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (finished_time); timer.finish (finished_time); assert_cmpuint (state_changed_call_count, GLib.CompareOperator.EQ, 2); assert_cmpuint (finished_call_count, GLib.CompareOperator.EQ, 1); assert_true (state_1.is_finished ()); assert_false (state_2.is_finished ()); assert_cmpvariant ( new GLib.Variant.int64 (state_1.finished_time), new GLib.Variant.int64 (finished_time) ); assert_cmpvariant ( new GLib.Variant.int64 (state_2.started_time), new GLib.Variant.int64 (finished_time) ); } public void test_timer_finished__wait_for_activity () { var idle_monitor = new Ft.IdleMonitor (); assert_true (idle_monitor.provider is Ft.DummyIdleMonitorProvider); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); session_manager.advance_to_state (Ft.State.SHORT_BREAK); var time_block_1 = session_manager.current_time_block; var finished_time = time_block_1.end_time; Ft.Timestamp.freeze_to (finished_time); timer.finish (finished_time); var time_block_2 = session_manager.current_time_block; assert_true (time_block_2.state == Ft.State.POMODORO); assert_true (timer.user_data == time_block_2); assert_false (timer.is_started ()); assert_false (timer.is_finished ()); // Simulate inactivity of 1 minute. var activity_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); idle_monitor.provider.became_active (); assert_true (session_manager.current_time_block == time_block_2); assert_true (timer.user_data == time_block_2); assert_true (timer.is_started ()); // Expect previous time-block to be extended by the inactivity time. assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (activity_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.offset), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (activity_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (activity_time) ); } /** * Test for `AdvancementMode.MANUAL` */ public void test_timer_finished__manual () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); settings.set_boolean ("confirm-starting-pomodoro", true); var timer = new Ft.Timer (); var session_manager = new Ft.SessionManager.with_timer (timer); var confirm_advancement_call_count = 0; session_manager.confirm_advancement.connect (() => { confirm_advancement_call_count++; }); timer.start (); var time_block_1 = session_manager.current_time_block; var finished_time = session_manager.current_time_block.end_time; Ft.Timestamp.freeze_to (finished_time); timer.finish (finished_time); assert_true (time_block_1.state == Ft.State.POMODORO); assert_true (timer.state.is_finished ()); // Confirm after 1 minute. var confirmation_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); // Note that timer.start() can't be used interchangeably with session_manager.advance(), // as the timer has already started. session_manager.advance (confirmation_time); var time_block_2 = session_manager.current_time_block; assert_true (time_block_2.state.is_break ()); assert_true (timer.user_data == time_block_2); assert_true (timer.is_started ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (confirmation_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.offset), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.end_time), new GLib.Variant.int64 (confirmation_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (confirmation_time) ); assert_cmpuint ( confirm_advancement_call_count, GLib.CompareOperator.EQ, 1U ); } } public class SessionManagerDatabaseTest : Tests.TestSuite { private Ft.Timer? timer; private Ft.SessionManager? session_manager; private Ft.TimezoneHistory? timezone_history; private GLib.TimeZone? new_york_timezone; private GLib.TimeZone? london_timezone; private GLib.MainLoop? main_loop; private uint timeout_id = 0; public SessionManagerDatabaseTest () { this.add_test ("save__empty_session", this.test_save__empty_session); this.add_test ("save__update_time_block_status", this.test_save__update_time_block_status); this.add_test ("save__update_time_range", this.test_save__update_time_range); this.add_test ("save__delete_extra_time_blocks", this.test_save__delete_extra_time_blocks); this.add_test ("save__delete_extra_gaps", this.test_save__delete_extra_gaps); this.add_test ("save__delete_empty_session", this.test_save__delete_empty_session); this.add_test ("save__advance_session", this.test_save__advance_session); this.add_test ("save__timer_start", this.test_save__timer_start); this.add_test ("save__timer_pause", this.test_save__timer_pause); this.add_test ("save__timer_rewind", this.test_save__timer_rewind); this.add_test ("restore__empty_database", this.test_restore__empty_database); this.add_test ("restore__empty_session", this.test_restore__empty_session); this.add_test ("restore__in_progress_time_block", this.test_restore__in_progress_time_block); this.add_test ("restore__uncompleted_time_block", this.test_restore__uncompleted_time_block); this.add_test ("restore__completed_time_block", this.test_restore__completed_time_block); this.add_test ("restore__multiple_time_blocks", this.test_restore__multiple_time_blocks); this.add_test ("restore__multiple_gaps", this.test_restore__multiple_gaps); this.add_test ("restore__missing_ongoing_gap", this.test_restore__missing_ongoing_gap); this.add_test ("restore__most_recent_session", this.test_restore__most_recent_session); this.add_test ("restore__completed_session", this.test_restore__completed_session); this.add_test ("restore__expired_session", this.test_restore__expired_session); this.add_test ("restore__confirm_advancement", this.test_restore__confirm_advancement); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); Ft.Database.open (); try { this.new_york_timezone = new GLib.TimeZone.identifier ("America/New_York"); this.london_timezone = new GLib.TimeZone.identifier ("Europe/London"); } catch (GLib.Error error) { assert_no_error (error); } this.main_loop = new GLib.MainLoop (); this.timezone_history = new Ft.TimezoneHistory (); this.timezone_history.insert (Ft.Timestamp.peek (), this.new_york_timezone); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); this.session_manager = new Ft.SessionManager.with_timer (this.timer); } public override void teardown () { var settings = Ft.get_settings (); settings.revert (); this.timer.reset (); Ft.Timer.set_default (null); this.session_manager = null; this.timer = null; this.timezone_history = null; this.main_loop = null; Ft.Database.close (); } private bool run_main_loop (uint timeout = 1000) { var success = true; if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.timeout_id = GLib.Timeout.add (timeout, () => { this.timeout_id = 0; this.main_loop.quit (); success = false; return GLib.Source.REMOVE; }); this.main_loop.run (); return success; } private void quit_main_loop () { if (this.timeout_id != 0) { GLib.Source.remove (this.timeout_id); this.timeout_id = 0; } this.main_loop.quit (); } private void run_save (Ft.SessionManager? session_manager = null) { if (session_manager == null) { session_manager = this.session_manager; } session_manager.save.begin ( (obj, res) => { assert_true (session_manager.save.end (res)); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); } private void run_restore (Ft.SessionManager? session_manager = null, int64 timestamp = Ft.Timestamp.UNDEFINED) { if (session_manager == null) { session_manager = this.session_manager; } session_manager.restore.begin ( timestamp, (obj, res) => { assert_true (session_manager.restore.end (res)); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); } /** * If session hasn't started yet, expect nothing to be saved. */ public void test_save__empty_session () { this.session_manager.ensure_session (); assert_true (this.session_manager.current_session.is_scheduled ()); this.run_save (); var repository = Ft.Database.get_repository (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0); } catch (GLib.Error error) { assert_no_error (error); } } /** * Test if `time_block.set_status()` call is propagated to an updated entry. */ public void test_save__update_time_block_status () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_session = session; this.run_save (); Ft.SessionEntry? initial_session_entry = null; Ft.TimeBlockEntry? initial_time_block_entry = null; try { initial_session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_nonnull (initial_session_entry); initial_time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_nonnull (initial_time_block_entry); // Modify just the time-block status, expect its entry to be updated. time_block.set_status (Ft.TimeBlockStatus.COMPLETED); } catch (GLib.Error error) { assert_no_error (error); } this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.id), new GLib.Variant.int64 (initial_session_entry.id) ); var time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.id), new GLib.Variant.int64 (initial_time_block_entry.id) ); assert_cmpstr (time_block_entry.status, GLib.CompareOperator.EQ, "completed"); } catch (GLib.Error error) { assert_no_error (error); } } /** * Test if changing time-block range is propagated to an updated entry. */ public void test_save__update_time_range () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_session = session; this.run_save (); Ft.SessionEntry? initial_session_entry = null; Ft.TimeBlockEntry? initial_time_block_entry = null; try { initial_session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_nonnull (initial_session_entry); initial_time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_nonnull (initial_time_block_entry); // Modify just the time-block range, expect its entry to be updated. time_block.move_by (Ft.Interval.MINUTE); } catch (GLib.Error error) { assert_no_error (error); } this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.id), new GLib.Variant.int64 (initial_session_entry.id) ); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.start_time), new GLib.Variant.int64 (session.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.end_time), new GLib.Variant.int64 (session.end_time) ); var time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.id), new GLib.Variant.int64 (initial_time_block_entry.id) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.start_time), new GLib.Variant.int64 (time_block.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.end_time), new GLib.Variant.int64 (time_block.end_time) ); } catch (GLib.Error error) { assert_no_error (error); } } /** * Simulate a hypothetical scenario that time-block exists in database but not in * session. Expect it to be removed. * * At the time this test is written we do not save scheduled time-blocks and we * don't remove time-blocks from session once they have started. */ public void test_save__delete_extra_time_blocks () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_session = session; this.run_save (); Ft.SessionEntry? session_entry = null; Ft.TimeBlockEntry? time_block_entry = null; try { session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_nonnull (session_entry); time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_nonnull (time_block_entry); var extra_time_block_entry = new Ft.TimeBlockEntry (); extra_time_block_entry.repository = repository; extra_time_block_entry.session_id = session_entry.id; extra_time_block_entry.start_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); extra_time_block_entry.end_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); extra_time_block_entry.state = "pomodoro"; extra_time_block_entry.status = "uncompleted"; extra_time_block_entry.intended_duration = 0; extra_time_block_entry.save_sync (); var results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2); } catch (GLib.Error error) { assert_no_error (error); } this.run_save (); try { var results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var remaining_time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (remaining_time_block_entry.id), new GLib.Variant.int64 (time_block_entry.id) ); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__delete_extra_gaps () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var gap = new Ft.Gap (); gap.set_time_range (now + 1 * Ft.Interval.MINUTE, now + 2 * Ft.Interval.MINUTE); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 26 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.add_gap (gap); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_session = session; this.run_save (); Ft.SessionEntry? session_entry = null; Ft.TimeBlockEntry? time_block_entry = null; Ft.GapEntry? gap_entry = null; try { session_entry = (Ft.SessionEntry?) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_nonnull (session_entry); time_block_entry = (Ft.TimeBlockEntry?) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_nonnull (time_block_entry); gap_entry = (Ft.GapEntry?) repository.find_one_sync ( typeof (Ft.GapEntry), null); assert_nonnull (gap_entry); var extra_gap_entry = new Ft.GapEntry (); extra_gap_entry.repository = repository; extra_gap_entry.time_block_id = time_block_entry.id; extra_gap_entry.start_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); extra_gap_entry.end_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); extra_gap_entry.flags = Ft.GapFlags.DEFAULT.to_string (); extra_gap_entry.save_sync (); var results = repository.find_sync (typeof (Ft.GapEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2); } catch (GLib.Error error) { assert_no_error (error); } // Only changed time-block will be saved. Force it. time_block.set_status (Ft.TimeBlockStatus.COMPLETED); this.run_save (); try { var results = repository.find_sync (typeof (Ft.GapEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var remaining_gap_entry = (Ft.GapEntry?) repository.find_one_sync ( typeof (Ft.GapEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (remaining_gap_entry.id), new GLib.Variant.int64 (gap_entry.id) ); } catch (GLib.Error error) { assert_no_error (error); } } /** * If session is modified and becomes empty, expect database entry to be removed. * * It's a hypothetical scenario. */ public void test_save__delete_empty_session () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_session = session; assert_false (this.timer.is_running ()); this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); } catch (GLib.Error error) { assert_no_error (error); } time_block.set_status (Ft.TimeBlockStatus.SCHEDULED); assert_true (session.is_scheduled ()); this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0); } catch (GLib.Error error) { assert_no_error (error); } time_block.set_status (Ft.TimeBlockStatus.COMPLETED); this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__advance_session () { var now = Ft.Timestamp.peek (); var repository = Ft.Database.get_repository (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (now, now + 5 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.UNCOMPLETED); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now + 10 * Ft.Interval.MINUTE, now + 35 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session_1 = new Ft.Session (); session_1.append (time_block_1); var session_2 = new Ft.Session (); session_2.append (time_block_2); this.session_manager.current_session = session_1; this.session_manager.current_session = session_2; this.run_save (); // Ensure that previous session has been saved as well as the current one. try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__timer_start () { this.session_manager.timer.start (); this.run_save (); // Expect one time-block that is in-progress to be saved. var repository = Ft.Database.get_repository (); var session = this.session_manager.current_session; var time_block = this.session_manager.current_time_block; try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.SessionEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var session_entry = (Ft.SessionEntry) repository.find_one_sync ( typeof (Ft.SessionEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.start_time), new GLib.Variant.int64 (session.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (session_entry.end_time), new GLib.Variant.int64 (session.end_time) ); var time_block_entry = (Ft.TimeBlockEntry) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.start_time), new GLib.Variant.int64 (time_block.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.end_time), new GLib.Variant.int64 (time_block.end_time) ); assert_cmpstr (time_block_entry.status, GLib.CompareOperator.EQ, "in-progress"); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__timer_pause () { var repository = Ft.Database.get_repository (); this.timer.start (); this.run_save (); // Pause timer Ft.Timestamp.advance (Ft.Interval.MINUTE); this.timer.pause (); this.run_save (); var session = this.session_manager.current_session; assert_false (Ft.Timestamp.is_undefined (session.end_time)); var time_block = this.session_manager.current_time_block; assert_false (Ft.Timestamp.is_undefined (time_block.end_time)); var gap = time_block.get_last_gap (); assert_true (Ft.Timestamp.is_undefined (gap.end_time)); var expected_time_block_id = (int64) 0; var expected_gap_id = (int64) 0; try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.GapEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var time_block_entry = (Ft.TimeBlockEntry) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.end_time), new GLib.Variant.int64 (time_block.end_time) ); var gap_entry = (Ft.GapEntry) repository.find_one_sync ( typeof (Ft.GapEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.time_block_id), new GLib.Variant.int64 (time_block_entry.id) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.start_time), new GLib.Variant.int64 (gap.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.end_time), new GLib.Variant.int64 (gap.end_time) ); expected_time_block_id = time_block_entry.id; expected_gap_id = gap_entry.id; } catch (GLib.Error error) { assert_no_error (error); } // Resume timer Ft.Timestamp.advance (Ft.Interval.MINUTE); this.timer.resume (); assert_true (Ft.Timestamp.is_defined (gap.end_time)); this.run_save (); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.GapEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var time_block_entry = (Ft.TimeBlockEntry) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.id), new GLib.Variant.int64 (expected_time_block_id) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.end_time), new GLib.Variant.int64 (time_block.end_time) ); var gap_entry = (Ft.GapEntry) repository.find_one_sync ( typeof (Ft.GapEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.id), new GLib.Variant.int64 (expected_gap_id) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.time_block_id), new GLib.Variant.int64 (time_block_entry.id) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.start_time), new GLib.Variant.int64 (gap.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.end_time), new GLib.Variant.int64 (gap.end_time) ); } catch (GLib.Error error) { assert_no_error (error); } } /** * Gaps may be normalized after using rewind. Expect unnecessary gaps to be removed. */ public void test_save__timer_rewind () { var repository = Ft.Database.get_repository (); var start_time = Ft.Timestamp.peek (); this.timer.start (start_time); this.run_save (); var pause_time = start_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (pause_time); this.timer.pause (pause_time); this.run_save (); var resume_time = pause_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (resume_time); this.timer.resume (resume_time); this.run_save (); var rewind_time = resume_time + Ft.Interval.MINUTE; Ft.Timestamp.freeze_to (rewind_time); this.timer.rewind (3 * Ft.Interval.MINUTE, rewind_time); this.run_save (); var time_block = this.session_manager.current_time_block; var gap = time_block.get_last_gap (); assert_cmpvariant ( new GLib.Variant.int64 (gap.duration), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE) ); try { Gom.ResourceGroup results; results = repository.find_sync (typeof (Ft.TimeBlockEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); results = repository.find_sync (typeof (Ft.GapEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var time_block_entry = (Ft.TimeBlockEntry) repository.find_one_sync ( typeof (Ft.TimeBlockEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.start_time), new GLib.Variant.int64 (time_block.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block_entry.end_time), new GLib.Variant.int64 (time_block.end_time) ); var gap_entry = (Ft.GapEntry) repository.find_one_sync ( typeof (Ft.GapEntry), null); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.start_time), new GLib.Variant.int64 (gap.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_entry.end_time), new GLib.Variant.int64 (gap.end_time) ); } catch (GLib.Error error) { assert_no_error (error); } } /** * Restoring from an empty database should not crash or set any session. */ public void test_restore__empty_database () { this.run_restore (); assert_null (this.session_manager.current_session); assert_null (this.session_manager.current_time_block); } /** * We delete sessions that are empty from database. However, expect an empty session * not to break things. */ public void test_restore__empty_session () { var session_entry = new Ft.SessionEntry (); session_entry.repository = Ft.Database.get_repository (); session_entry.start_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); session_entry.end_time = Ft.Timestamp.advance (25 * Ft.Interval.MINUTE); try { session_entry.save_sync (); } catch (GLib.Error error) { assert_no_error (error); } this.run_restore (); assert_null (this.session_manager.current_session); assert_null (this.session_manager.current_time_block); } /** * Restore a session with a single in-progress time block. */ public void test_restore__in_progress_time_block () { var timestamp = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); time_block.notify["start-time"].connect ( () => { assert_not_reached (); }); var gap_1 = new Ft.Gap (); gap_1.set_time_range (time_block.start_time + Ft.Interval.MINUTE, time_block.start_time + 3 * Ft.Interval.MINUTE); time_block.add_gap (gap_1); time_block.end_time += gap_1.duration; var gap_2 = new Ft.Gap.with_start_time ( time_block.start_time + 4 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); var session = new Ft.Session (); session.append (time_block); Ft.Timestamp.freeze_to (gap_2.start_time); this.session_manager.current_time_block = time_block; assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); var save_time = gap_2.start_time + 5 * Ft.Interval.MINUTE; var restore_time = save_time + 30 * Ft.Interval.MINUTE; // Save Ft.Timestamp.freeze_to (save_time); this.run_save (); // Restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); Ft.Timestamp.freeze_to (restore_time); this.run_restore (new_session_manager, restore_time); var restored_session = new_session_manager.current_session; var restored_time_block = new_session_manager.current_time_block; var restored_gap = new_session_manager.current_gap; assert_nonnull (restored_session); assert_nonnull (restored_time_block); assert_nonnull (restored_gap); assert_cmpvariant ( new GLib.Variant.int64 (restored_session.start_time), new GLib.Variant.int64 (session.start_time) ); assert_cmpuint ( restored_session.count_visible_cycles (), GLib.CompareOperator.EQ, session.count_visible_cycles () ); assert_true (restored_time_block.state == Ft.State.POMODORO); assert_true (restored_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block.start_time), new GLib.Variant.int64 (time_block.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block.get_intended_duration ()), new GLib.Variant.int64 (time_block.get_intended_duration ()) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block.calculate_elapsed (restore_time)), new GLib.Variant.int64 (time_block.calculate_elapsed (save_time)) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block.calculate_remaining (restore_time)), new GLib.Variant.int64 (time_block.calculate_remaining (save_time)) ); assert_true (restored_gap.flags == gap_2.flags); assert_cmpvariant ( new GLib.Variant.int64 (restored_gap.start_time), new GLib.Variant.int64 (gap_2.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_gap.end_time), new GLib.Variant.int64 (gap_2.end_time) ); var expected_elapsed = time_block.calculate_elapsed (); var expected_timer_state = Ft.TimerState () { duration = time_block.get_intended_duration (), offset = gap_2.start_time - time_block.start_time - expected_elapsed, started_time = time_block.start_time, paused_time = gap_2.start_time, finished_time = Ft.Timestamp.UNDEFINED, user_data = restored_time_block }; assert_cmpvariant ( timer.state.to_variant (), expected_timer_state.to_variant () ); } /** * Restore a session with an uncompleted time block. */ public void test_restore__uncompleted_time_block () { var timestamp = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); var session = new Ft.Session (); session.append (time_block); this.session_manager.current_time_block = time_block; timestamp = Ft.Timestamp.advance (Ft.Interval.MINUTE); Ft.Timestamp.freeze_to (timestamp); this.session_manager.current_time_block = null; assert_true (time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); timestamp = Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); this.run_restore (new_session_manager, timestamp); var restored_session = new_session_manager.current_session; var restored_time_block = new_session_manager.current_time_block; assert_nonnull (restored_session); assert_null (restored_time_block); assert_cmpvariant ( new GLib.Variant.int64 (restored_session.start_time), new GLib.Variant.int64 (session.start_time) ); assert_cmpuint ( restored_session.count_visible_cycles (), GLib.CompareOperator.EQ, session.count_visible_cycles () ); restored_time_block = restored_session.get_first_time_block (); assert_true (restored_time_block.get_status () == Ft.TimeBlockStatus.UNCOMPLETED); new_timer.start (); assert_true (new_session_manager.current_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_true (restored_session.get_previous_time_block (new_session_manager.current_time_block) == restored_time_block); } /** * Restore a session with in-progress time-block when the timer reached finish and we * await advancement confirmation. */ public void test_restore__completed_time_block () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-break", true); var timestamp = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var session = new Ft.Session (); session.append (time_block); Ft.Timestamp.freeze_to (time_block.end_time); this.session_manager.current_time_block = time_block; assert_true (time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (timestamp) ); assert_true (this.timer.is_finished ()); Ft.Timestamp.freeze_to (time_block.end_time + 10 * Ft.Interval.MINUTE); this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); Ft.Timestamp.freeze_to (time_block.end_time + 20 * Ft.Interval.MINUTE); this.run_restore (new_session_manager); var restored_session = new_session_manager.current_session; var restored_time_block = new_session_manager.current_time_block; var restored_gap = new_session_manager.current_gap; assert_nonnull (restored_session); assert_nonnull (restored_time_block); assert_null (restored_gap); assert_true (restored_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_true (this.timer.is_finished ()); assert_cmpuint ( restored_session.count_visible_cycles (), GLib.CompareOperator.EQ, session.count_visible_cycles () ); } /** * Restore a session with multiple time blocks (only in-progress ones are restored). */ public void test_restore__multiple_time_blocks () { var timestamp = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (timestamp, timestamp + 25 * Ft.Interval.MINUTE); time_block_1.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.set_time_range (timestamp + 25 * Ft.Interval.MINUTE, timestamp + 30 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (5 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap.with_start_time (timestamp + 26 * Ft.Interval.MINUTE); time_block_2.add_gap (gap); var session = new Ft.Session (); session.append (time_block_1); session.append (time_block_2); Ft.Timestamp.freeze_to (gap.start_time); this.session_manager.current_time_block = time_block_2; this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); this.run_restore (new_session_manager); assert_nonnull (new_session_manager.current_session); assert_nonnull (new_session_manager.current_time_block); assert_nonnull (new_session_manager.current_gap); assert_true (new_session_manager.current_state == Ft.State.SHORT_BREAK); assert_true (new_timer.is_paused ()); var restored_session = new_session_manager.current_session; var restored_time_block_1 = restored_session.get_nth_time_block (0); var restored_time_block_2 = restored_session.get_nth_time_block (1); assert_nonnull (restored_time_block_1); assert_true (restored_time_block_1.state == Ft.State.POMODORO); assert_true (restored_time_block_1.get_status () == Ft.TimeBlockStatus.COMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_1.start_time), new GLib.Variant.int64 (time_block_1.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_1.end_time), new GLib.Variant.int64 (time_block_1.end_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_1.get_intended_duration ()), new GLib.Variant.int64 (time_block_1.get_intended_duration ()) ); assert_cmpfloat ( restored_time_block_1.get_weight (), GLib.CompareOperator.EQ, time_block_1.get_weight () ); assert_nonnull (restored_time_block_2); assert_true (restored_time_block_2.state == Ft.State.SHORT_BREAK); assert_true (restored_time_block_2.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_2.start_time), new GLib.Variant.int64 (time_block_2.start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_2.end_time), new GLib.Variant.int64 (time_block_2.end_time) ); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block_2.get_intended_duration ()), new GLib.Variant.int64 (time_block_2.get_intended_duration ()) ); assert_cmpfloat ( restored_time_block_2.get_weight (), GLib.CompareOperator.EQ, time_block_2.get_weight () ); assert_cmpuint ( restored_session.count_visible_cycles (), GLib.CompareOperator.EQ, session.count_visible_cycles () ); } /** * Restore a session with gaps in time blocks. */ public void test_restore__multiple_gaps () { var timestamp = Ft.Timestamp.peek (); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (timestamp, timestamp + 50 * Ft.Interval.MINUTE); time_block_1.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var gap_1 = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap_1.set_time_range (timestamp + 5 * Ft.Interval.MINUTE, timestamp + 30 * Ft.Interval.MINUTE); time_block_1.add_gap (gap_1); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (timestamp + 60 * Ft.Interval.MINUTE, timestamp + 87 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap_2 = new Ft.Gap (Ft.GapFlags.SLEEP); gap_2.set_time_range (timestamp + 65 * Ft.Interval.MINUTE, timestamp + 67 * Ft.Interval.MINUTE); time_block_2.add_gap (gap_2); var gap_3 = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap_3.start_time = timestamp + 70 * Ft.Interval.MINUTE; time_block_2.add_gap (gap_3); var session = new Ft.Session (); session.append (time_block_1); session.append (time_block_2); Ft.Timestamp.freeze_to (gap_3.start_time); this.session_manager.current_time_block = time_block_2; this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); this.run_restore (new_session_manager); assert_nonnull (new_session_manager.current_session); assert_nonnull (new_session_manager.current_time_block); assert_nonnull (new_session_manager.current_gap); assert_true (new_session_manager.current_state == Ft.State.POMODORO); assert_true (new_timer.is_paused ()); var restored_session = new_session_manager.current_session; var restored_time_block_1 = restored_session.get_nth_time_block (0); var restored_time_block_2 = restored_session.get_nth_time_block (1); var restored_gap_1 = restored_time_block_1.get_nth_gap (0); var restored_gap_2 = restored_time_block_2.get_nth_gap (0); var restored_gap_3 = restored_time_block_2.get_nth_gap (1); assert_nonnull (restored_gap_1); assert_true (restored_gap_1.flags == Ft.GapFlags.INTERRUPTION); assert_cmpvariant ( restored_gap_1.start_time, gap_1.start_time ); assert_cmpvariant ( restored_gap_1.end_time, gap_1.end_time ); assert_nonnull (restored_gap_2); assert_true (restored_gap_2.flags == Ft.GapFlags.SLEEP); assert_cmpvariant ( restored_gap_2.start_time, gap_2.start_time ); assert_cmpvariant ( restored_gap_2.end_time, gap_2.end_time ); assert_nonnull (restored_gap_3); assert_true (restored_gap_3.flags == Ft.GapFlags.INTERRUPTION); assert_cmpvariant ( restored_gap_3.start_time, gap_3.start_time ); assert_cmpvariant ( restored_gap_3.end_time, Ft.Timestamp.UNDEFINED ); assert_cmpfloat ( restored_time_block_1.get_weight (), GLib.CompareOperator.EQ, time_block_1.get_weight () ); assert_cmpuint ( restored_session.count_visible_cycles (), GLib.CompareOperator.EQ, session.count_visible_cycles () ); } /** * Expect session to be paused when shutting down the app. * When the app hasn't closed properly, expect it to rewind to the last known * position. It's better to under-report spent time. */ public void test_restore__missing_ongoing_gap () { var timestamp = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 27 * Ft.Interval.MINUTE); time_block.set_intended_duration (25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (timestamp + 5 * Ft.Interval.MINUTE, timestamp + 7 * Ft.Interval.MINUTE); time_block.add_gap (gap); var session = new Ft.Session (); session.append (time_block); Ft.Timestamp.freeze_to (gap.end_time + Ft.Interval.MINUTE); this.session_manager.current_time_block = time_block; this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); Ft.Timestamp.freeze_to (gap.end_time + 5 * Ft.Interval.MINUTE); this.run_restore (new_session_manager); var restored_session = new_session_manager.current_session; assert_nonnull (restored_session); var restored_time_block = new_session_manager.current_time_block; assert_nonnull (restored_time_block); var restored_gap = restored_time_block.get_last_gap (); assert_nonnull (restored_gap); assert_cmpvariant ( restored_gap.start_time, gap.start_time ); assert_cmpvariant ( restored_gap.end_time, Ft.Timestamp.UNDEFINED ); } /** * When multiple sessions exist, restore should load the most recent one. */ public void test_restore__most_recent_session () { // Create and save first session (older) var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.start_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); time_block_1.end_time = Ft.Timestamp.advance (25 * Ft.Interval.MINUTE); time_block_1.set_intended_duration (25 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var session_1 = new Ft.Session (); session_1.append (time_block_1); this.session_manager.current_session = session_1; this.run_save (); // Create and save second session (more recent) Ft.Timestamp.advance (1 * Ft.Interval.HOUR); var time_block_2 = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block_2.start_time = Ft.Timestamp.advance (Ft.Interval.MINUTE); time_block_2.end_time = Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (5 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (); gap.start_time = time_block_2.start_time + 1 * Ft.Interval.MINUTE; time_block_2.add_gap (gap); var session_2 = new Ft.Session (); session_2.append (time_block_2); this.session_manager.current_session = session_2; this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); this.run_restore (new_session_manager); var restored_session = new_session_manager.current_session; assert_nonnull (restored_session); assert_cmpvariant ( new GLib.Variant.int64 (restored_session.start_time), new GLib.Variant.int64 (session_2.start_time) ); var restored_time_block = new_session_manager.current_time_block; assert_nonnull (new_session_manager.current_time_block); assert_true (restored_time_block.state == Ft.State.SHORT_BREAK); assert_true (restored_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); } public void test_restore__completed_session () { var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.start_time = Ft.Timestamp.peek (); time_block_1.end_time = Ft.Timestamp.advance (25 * Ft.Interval.MINUTE); time_block_1.set_intended_duration (time_block_1.duration); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var time_block_2 = new Ft.TimeBlock (Ft.State.LONG_BREAK); time_block_2.start_time = Ft.Timestamp.peek (); time_block_2.end_time = Ft.Timestamp.advance (15 * Ft.Interval.MINUTE); time_block_2.set_intended_duration (time_block_2.duration); time_block_2.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block_1); session.append (time_block_2); assert_true (session.is_completed ()); this.session_manager.current_session = session; this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); this.run_restore (new_session_manager); assert_null (new_session_manager.current_session); assert_null (new_session_manager.current_time_block); } public void test_restore__expired_session () { var timestamp = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 25 * Ft.Interval.MINUTE); time_block.set_intended_duration (time_block.duration); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.start_time = timestamp + 5 * Ft.Interval.MINUTE; time_block.add_gap (gap); var session = new Ft.Session (); session.append (time_block); assert_false (session.is_completed ()); Ft.Timestamp.freeze_to (timestamp); this.session_manager.current_time_block = time_block; assert_true (Ft.Timestamp.is_defined (session.expiry_time)); this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); Ft.Timestamp.freeze_to (session.expiry_time); this.run_restore (new_session_manager); assert_null (new_session_manager.current_session); assert_null (new_session_manager.current_time_block); } public void test_restore__confirm_advancement () { var settings = Ft.get_settings (); settings.set_boolean ("confirm-starting-pomodoro", true); this.session_manager.ensure_session (); var session = this.session_manager.current_session; var time_block_1 = session.get_nth_time_block (0U); // Pomodoro time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var time_block_2 = session.get_nth_time_block (1U); // Short break time_block_2.set_status (Ft.TimeBlockStatus.IN_PROGRESS); Ft.Timestamp.freeze_to (time_block_2.start_time); this.session_manager.current_time_block = time_block_2; Ft.Timestamp.freeze_to (time_block_2.end_time); this.timer.finish (time_block_2.end_time); Ft.Timestamp.freeze_to (time_block_2.end_time + Ft.Interval.MINUTE); this.run_save (); // Create a new session manager to test restore var new_timer = new Ft.Timer (); var new_session_manager = new Ft.SessionManager.with_timer (new_timer); var confirm_advancement_call_count = 0U; new_session_manager.confirm_advancement.connect ( () => { confirm_advancement_call_count++; }); Ft.Timestamp.freeze_to (time_block_2.end_time + 10 * Ft.Interval.MINUTE); this.run_restore (new_session_manager); var restored_session = new_session_manager.current_session; var restored_time_block = new_session_manager.current_time_block; var restored_gap = new_session_manager.current_gap; assert_nonnull (restored_session); assert_nonnull (restored_time_block); assert_null (restored_gap); assert_true (restored_time_block.state == time_block_2.state); assert_true (restored_time_block.get_status () == Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpvariant ( new GLib.Variant.int64 (restored_time_block.end_time), new GLib.Variant.int64 (time_block_2.end_time) ); assert_true (this.timer.is_finished ()); assert_cmpuint (confirm_advancement_call_count, GLib.CompareOperator.EQ, 1U); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.SessionManagerTest (), new Tests.SessionManagerTimerTest (), new Tests.SessionManagerDatabaseTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-session.vala000066400000000000000000001173131520625676500233340ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class SessionTemplateTest : Tests.TestSuite { public SessionTemplateTest () { this.add_test ("calculate_break_percentage", this.test_calculate_break_percentage); } public void test_calculate_break_percentage () { var session_template_1 = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 0 * Ft.Interval.MINUTE, long_break_duration = 0 * Ft.Interval.MINUTE, cycles = 4 }; assert_cmpfloat_with_epsilon (session_template_1.calculate_break_percentage (), 0.0, 0.0001); var session_template_2 = Ft.SessionTemplate () { pomodoro_duration = 0 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 4 }; assert_cmpfloat_with_epsilon (session_template_2.calculate_break_percentage (), 100.0, 0.0001); var session_template_3 = Ft.SessionTemplate () { pomodoro_duration = 3 * Ft.Interval.MINUTE, short_break_duration = 1 * Ft.Interval.MINUTE, long_break_duration = 1 * Ft.Interval.MINUTE, cycles = 1 }; // 1 / (3 + 1) assert_cmpfloat_with_epsilon (session_template_3.calculate_break_percentage (), 25.0, 0.0001); var session_template_4 = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 10 * Ft.Interval.MINUTE, cycles = 4 }; // (5 + 5 + 5 + 10) / (25 + 5 + 25 + 5 + 25 + 5 + 25 + 10) assert_cmpfloat_with_epsilon (session_template_4.calculate_break_percentage (), 20.0, 0.0001); } } public class SessionTest : Tests.TestSuite { private Ft.SessionTemplate session_template = Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 4 }; public SessionTest () { this.add_test ("new", this.test_new); this.add_test ("new_from_template", this.test_new_from_template); this.add_test ("duration", this.test_duration); this.add_test ("start_time", this.test_start_time); this.add_test ("end_time", this.test_end_time); this.add_test ("get_first_time_block", this.test_get_first_time_block); this.add_test ("get_last_time_block", this.test_get_last_time_block); this.add_test ("get_next_time_block", this.test_get_next_time_block); this.add_test ("get_previous_time_block", this.test_get_previous_time_block); this.add_test ("append", this.test_append); this.add_test ("prepend", this.test_prepend); this.add_test ("insert_before", this.test_insert_before); this.add_test ("insert_after", this.test_insert_after); this.add_test ("contains", this.test_contains); this.add_test ("move_by", this.test_move_by); this.add_test ("move_to", this.test_move_to); this.add_test ("remove", this.test_remove); this.add_test ("remove_before", this.test_remove_before); this.add_test ("remove_after", this.test_remove_after); this.add_test ("set_time_block_status__completed", this.test_set_time_block_status__completed); this.add_test ("set_time_block_status__uncompleted", this.test_set_time_block_status__uncompleted); this.add_test ("is_expired", this.test_is_expired); this.add_test ("calculate_elapsed", this.test_calculate_elapsed); this.add_test ("calculate_remaining", this.test_calculate_remaining); // this.add_test ("calculate_progress", this.test_calculate_progress); // this.add_test ("calculate_pomodoro_break_ratio", this.test_calculate_pomodoro_break_ratio); this.add_test ("calculate_break_ratio", this.test_calculate_break_ratio); this.add_test ("get_cycles", this.test_get_cycles); this.add_test ("get_cycles__remove_time_block", this.test_get_cycles__remove_time_block); // TODO: Tests for signals // this.add_test ("freeze_changed", this.test_freeze_changed); // this.add_test ("changed_signal", this.test_changed_signal); // this.add_test ("added_signal", this.test_time_block_added_signal); // this.add_test ("removed_signal", this.test_time_block_removed_signal); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); } public override void teardown () { Ft.Timestamp.thaw (); var settings = Ft.get_settings (); settings.revert (); } /** * Check constructor `Session()`. * * Expect session not to have any time-blocks. */ public void test_new () { var session = new Ft.Session (); var first_time_block = session.get_first_time_block (); assert_null (first_time_block); var last_time_block = session.get_last_time_block (); assert_null (last_time_block); assert_cmpvariant ( new GLib.Variant.int64 (session.start_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (session.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); // assert_cmpuint (session.get_cycles ().length (), GLib.CompareOperator.EQ, 0); } /** * Check constructor `Session.from_template()`. * * Expect session to have time-blocks defined according to settings. */ public void test_new_from_template () { var now = Ft.Timestamp.advance (0); var template = this.session_template; var session = new Ft.Session.from_template (template); Ft.TimeBlock[] time_blocks = {}; session.@foreach ((time_block) => { time_blocks += time_block; }); assert_cmpuint (time_blocks.length, GLib.CompareOperator.EQ, template.cycles * 2); // Expect: // - pomodoro / break states to be interleaved // - last break to be a long one // - start times to align // - `cycle` to be indexed form 1 var expected_start_time = now; for (uint index=0; index < time_blocks.length; index++) { var time_block = time_blocks[index]; if ((index & 1) == 0) { assert_true (time_block.state == Ft.State.POMODORO); assert_cmpvariant ( new GLib.Variant.int64 (time_block.duration), new GLib.Variant.int64 (template.pomodoro_duration) ); } else if (index < time_blocks.length - 1) { assert_true (time_block.state == Ft.State.SHORT_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block.duration), new GLib.Variant.int64 (template.short_break_duration) ); } else { assert_true (time_block.state == Ft.State.LONG_BREAK); assert_cmpvariant ( new GLib.Variant.int64 (time_block.duration), new GLib.Variant.int64 (template.long_break_duration) ); } assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_true (time_block.session == session); // assert_cmpuint (meta.cycle, GLib.CompareOperator.EQ, 1 + (index >> 1)); expected_start_time += time_block.duration; } assert_cmpvariant ( new GLib.Variant.int64 (session.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (session.end_time), new GLib.Variant.int64 ( session.start_time + ( template.pomodoro_duration * template.cycles + template.short_break_duration * (template.cycles - 1) + template.long_break_duration ) ) ); // assert_cmpuint (session.get_cycles ().length (), GLib.CompareOperator.EQ, template.cycles); } public void test_duration () { var notify_duration_emitted = 0; var now = Ft.Timestamp.advance (0); var session_0 = new Ft.Session (); assert_cmpvariant ( new GLib.Variant.int64 (session_0.duration), new GLib.Variant.int64 (0) ); var session_1 = new Ft.Session (); session_1.notify["duration"].connect (() => { notify_duration_emitted++; }); var time_block_1 = new Ft.TimeBlock (); time_block_1.set_time_range (now, now + 2 * Ft.Interval.MINUTE); session_1.append (time_block_1); assert_cmpvariant ( new GLib.Variant.int64 (session_1.duration), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE) ); assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 1); var time_block_2 = new Ft.TimeBlock (); time_block_2.set_time_range (now, now + 3 * Ft.Interval.MINUTE); session_1.append (time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (session_1.duration), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE) ); assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 2); // Adding a gap between time-blocks should increase duration. time_block_2.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.duration), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE) ); assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3); // Moving a session shouldn't emit notify signal. session_1.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.duration), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE) ); assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3); // Uncompleted time-blocks should be included in duration. session_1.set_time_block_status (time_block_2, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (session_1.duration), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE) ); assert_cmpuint (notify_duration_emitted, GLib.CompareOperator.EQ, 3); } public void test_start_time () { var notify_start_time_emitted = 0; var now = Ft.Timestamp.advance (0); var session_0 = new Ft.Session (); assert_cmpvariant ( new GLib.Variant.int64 (session_0.start_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); var session_1 = new Ft.Session (); session_1.notify["start-time"].connect (() => { notify_start_time_emitted++; }); var time_block_2 = new Ft.TimeBlock (); time_block_2.set_time_range (now + 4 * Ft.Interval.MINUTE, now + 5 * Ft.Interval.MINUTE); session_1.prepend (time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (time_block_2.start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 1); var time_block_1 = new Ft.TimeBlock (); time_block_1.set_time_range (now + 3 * Ft.Interval.MINUTE, now + 4 * Ft.Interval.MINUTE); session_1.prepend (time_block_1); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (time_block_1.start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 2); // Moving first time-block should affect start_time. var expected_start_time = time_block_1.start_time + Ft.Interval.MINUTE; time_block_1.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 3); // Moving a session should emit notify signal. expected_start_time = session_1.start_time + Ft.Interval.MINUTE; session_1.move_to (expected_start_time); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4); // Moving second time-block should not affect start_time. expected_start_time = session_1.start_time; time_block_2.move_to (time_block_2.start_time + Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4); // Uncompleted time-blocks should be included in start_time. expected_start_time = session_1.start_time; session_1.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (session_1.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (notify_start_time_emitted, GLib.CompareOperator.EQ, 4); } public void test_end_time () { var notify_end_time_emitted = 0; var now = Ft.Timestamp.advance (0); var session_0 = new Ft.Session (); assert_cmpvariant ( new GLib.Variant.int64 (session_0.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); var session_1 = new Ft.Session (); session_1.notify["end-time"].connect (() => { notify_end_time_emitted++; }); var time_block_1 = new Ft.TimeBlock (); time_block_1.set_time_range (now + 3 * Ft.Interval.MINUTE, now + 4 * Ft.Interval.MINUTE); session_1.append (time_block_1); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (time_block_1.end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 1); var time_block_2 = new Ft.TimeBlock (); time_block_2.set_time_range (now + 4 * Ft.Interval.MINUTE, now + 5 * Ft.Interval.MINUTE); session_1.append (time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (time_block_2.end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 2); // Moving second time-block should affect end_time. var expected_end_time = time_block_2.end_time + Ft.Interval.MINUTE; time_block_2.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (expected_end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 3); // Moving a session should emit notify signal. expected_end_time = session_1.end_time + Ft.Interval.MINUTE; session_1.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (expected_end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4); // Moving first time-block should not affect end_time. expected_end_time = session_1.end_time; time_block_1.move_by (-Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (expected_end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4); // Uncompleted time-blocks should be included in end_time. expected_end_time = session_1.end_time; session_1.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpvariant ( new GLib.Variant.int64 (session_1.end_time), new GLib.Variant.int64 (expected_end_time) ); assert_cmpuint (notify_end_time_emitted, GLib.CompareOperator.EQ, 4); } public void test_get_first_time_block () { var time_blocks = new Ft.TimeBlock[0]; var session = new Ft.Session.from_template (this.session_template); session.@foreach ((time_block) => { time_blocks += time_block; }); assert_true (session.get_first_time_block () == time_blocks[0]); var empty_session = new Ft.Session (); assert_null (empty_session.get_first_time_block ()); } public void test_get_last_time_block () { var time_blocks = new Ft.TimeBlock[0]; var session = new Ft.Session.from_template (this.session_template); session.@foreach ((time_block) => { time_blocks += time_block; }); assert_true (session.get_last_time_block () == time_blocks[7]); var empty_session = new Ft.Session (); assert_null (empty_session.get_last_time_block ()); } public void test_get_next_time_block () { var time_blocks = new Ft.TimeBlock[0]; var session = new Ft.Session.from_template (this.session_template); session.@foreach ((time_block) => { time_blocks += time_block; }); assert_true ( session.get_next_time_block (time_blocks[0]) == time_blocks[1] ); assert_true ( session.get_next_time_block (time_blocks[1]) == time_blocks[2] ); assert_null ( session.get_next_time_block (time_blocks[7]) ); assert_null ( session.get_next_time_block (new Ft.TimeBlock (Ft.State.POMODORO)) ); } public void test_get_previous_time_block () { var time_blocks = new Ft.TimeBlock[0]; var session = new Ft.Session.from_template (this.session_template); session.@foreach ((time_block) => { time_blocks += time_block; }); assert_true ( session.get_previous_time_block (time_blocks[2]) == time_blocks[1] ); assert_true ( session.get_previous_time_block (time_blocks[1]) == time_blocks[0] ); assert_null ( session.get_previous_time_block (time_blocks[0]) ); assert_null ( session.get_previous_time_block (new Ft.TimeBlock (Ft.State.POMODORO)) ); } public void test_append () { var session = new Ft.Session (); var now = Ft.Timestamp.from_now (); var added_emitted = 0; session.added.connect (() => { added_emitted++; }); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var time_block_1 = new Ft.TimeBlock (); time_block_1.set_time_range (now, now + Ft.Interval.MINUTE); session.append (time_block_1); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); assert_true (session.get_last_time_block () == time_block_1); assert_cmpvariant ( new GLib.Variant.int64 (time_block_1.start_time), new GLib.Variant.int64 (now) ); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (now, now + Ft.Interval.MINUTE); session.append (time_block_2); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); assert_true (session.get_last_time_block () == time_block_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block_2.start_time), new GLib.Variant.int64 (time_block_1.end_time) ); } public void test_prepend () { var session = new Ft.Session (); var added_emitted = 0; session.added.connect (() => { added_emitted++; }); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var time_block_1 = new Ft.TimeBlock (); session.prepend (time_block_1); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); assert_true (session.get_first_time_block () == time_block_1); var time_block_2 = new Ft.TimeBlock (); session.prepend (time_block_2); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); assert_true (session.get_first_time_block () == time_block_2); } public void test_insert_before () { var session = new Ft.Session.from_template (this.session_template); var added_emitted = 0; session.added.connect (() => { added_emitted++; }); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var reference_time_block = session.get_last_time_block (); var time_block_1 = new Ft.TimeBlock (); session.insert_before (time_block_1, reference_time_block); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); assert_true (session.get_previous_time_block (reference_time_block) == time_block_1); var time_block_2 = new Ft.TimeBlock (); session.insert_before (time_block_2, session.get_first_time_block ()); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); assert_true (session.get_first_time_block () == time_block_2); } public void test_insert_after () { var session = new Ft.Session.from_template (this.session_template); var added_emitted = 0; session.added.connect (() => { added_emitted++; }); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var reference_time_block = session.get_last_time_block (); var time_block_1 = new Ft.TimeBlock (); session.insert_after (time_block_1, reference_time_block); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); assert_true (session.get_next_time_block (reference_time_block) == time_block_1); var time_block_2 = new Ft.TimeBlock (); session.insert_after (time_block_2, session.get_last_time_block ()); assert_cmpuint (added_emitted, GLib.CompareOperator.EQ, 2); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); assert_true (session.get_last_time_block () == time_block_2); } public void test_contains () { var session = new Ft.Session.from_template (this.session_template); var time_block = new Ft.TimeBlock (); assert_false (session.contains (time_block)); session.append (time_block); assert_true (session.contains (time_block)); } public void test_move_by () { var session = new Ft.Session.from_template (this.session_template); var first_time_block = session.get_first_time_block (); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var expected_start_time = session.start_time + Ft.Interval.MINUTE; session.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (session.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (first_time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); } public void test_move_to () { var session = new Ft.Session.from_template (this.session_template); var first_time_block = session.get_first_time_block (); var changed_emitted = 0; session.changed.connect (() => { changed_emitted++; }); var expected_start_time = session.start_time + Ft.Interval.MINUTE; session.move_to (expected_start_time); assert_cmpvariant ( new GLib.Variant.int64 (session.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpvariant ( new GLib.Variant.int64 (first_time_block.start_time), new GLib.Variant.int64 (expected_start_time) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); } public void test_remove () { var removed_emitted = 0; var weak_notify_emitted = 0; var session = new Ft.Session.from_template (this.session_template); session.removed.connect (() => { removed_emitted++; }); var time_block = session.get_last_time_block (); time_block.weak_ref (() => { weak_notify_emitted++; }); assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 2); session.remove (time_block); assert_false (session.contains (time_block)); assert_cmpuint (time_block.ref_count, GLib.CompareOperator.EQ, 1); time_block = null; assert_cmpuint (removed_emitted, GLib.CompareOperator.EQ, 1); assert_cmpuint (weak_notify_emitted, GLib.CompareOperator.EQ, 1); } public void test_remove_before () { var removed_emitted = 0; var weak_notify_emitted = 0; var session = new Ft.Session.from_template (this.session_template); session.removed.connect (() => { removed_emitted++; }); var time_block_1 = session.get_first_time_block (); time_block_1.weak_ref (() => { weak_notify_emitted++; }); var time_block_2 = session.get_last_time_block (); assert_cmpuint (time_block_1.ref_count, GLib.CompareOperator.EQ, 2); session.remove_before (time_block_2); assert_false (session.contains (time_block_1)); assert_true (session.contains (time_block_2)); assert_cmpuint (time_block_1.ref_count, GLib.CompareOperator.EQ, 1); time_block_1 = null; assert_cmpuint (removed_emitted, GLib.CompareOperator.EQ, 7); assert_cmpuint (weak_notify_emitted, GLib.CompareOperator.EQ, 1); } public void test_remove_after () { var removed_emitted = 0; var weak_notify_emitted = 0; var session = new Ft.Session.from_template (this.session_template); session.removed.connect (() => { removed_emitted++; }); var time_block_1 = session.get_first_time_block (); var time_block_2 = session.get_last_time_block (); time_block_2.weak_ref (() => { weak_notify_emitted++; }); assert_cmpuint (time_block_2.ref_count, GLib.CompareOperator.EQ, 2); session.remove_after (time_block_1); assert_true (session.contains (time_block_1)); assert_false (session.contains (time_block_2)); assert_cmpuint (time_block_2.ref_count, GLib.CompareOperator.EQ, 1); time_block_2 = null; assert_cmpuint (removed_emitted, GLib.CompareOperator.EQ, 7); assert_cmpuint (weak_notify_emitted, GLib.CompareOperator.EQ, 1); } public void test_set_time_block_status__completed () { var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); var time_block_3 = session.get_nth_time_block (2); session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.COMPLETED); assert_cmpuint (time_block_1.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.COMPLETED); assert_cmpuint (time_block_2.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.SCHEDULED); session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED); session.set_time_block_status (time_block_3, Ft.TimeBlockStatus.COMPLETED); assert_cmpuint (time_block_3.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.COMPLETED); assert_cmpuint (time_block_2.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.UNCOMPLETED); // change from scheduled assert_cmpuint (time_block_1.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.UNCOMPLETED); // no change } public void test_set_time_block_status__uncompleted () { var session = new Ft.Session.from_template (this.session_template); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); var time_block_3 = session.get_nth_time_block (2); session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint (time_block_1.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint (time_block_2.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.SCHEDULED); session.set_time_block_status (time_block_1, Ft.TimeBlockStatus.COMPLETED); session.set_time_block_status (time_block_3, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint (time_block_3.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint (time_block_2.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.UNCOMPLETED); // change from scheduled assert_cmpuint (time_block_1.get_status (), GLib.CompareOperator.EQ, Ft.TimeBlockStatus.COMPLETED); // no change } public void test_is_expired () { var now = Ft.Timestamp.advance (0); var session = new Ft.Session.from_template (this.session_template); assert_false (session.is_expired (now)); session.expiry_time = now + Ft.Interval.MINUTE; assert_false (session.is_expired (now)); assert_true (session.is_expired (now + Ft.Interval.MINUTE)); } public void test_calculate_elapsed () { // TODO } public void test_calculate_remaining () { // TODO } // public void test_calculate_progress () // { // } // public void test_calculate_pomodoro_break_ratio () // { // var session = new Ft.Session (); // // assert_cmpfloat_with_epsilon ( // session.calculate_pomodoro_break_ratio (), // double.INFINITY, // 0.0001 // ); // } public void test_calculate_break_ratio () { var session = new Ft.Session.from_template ( Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 10 * Ft.Interval.MINUTE, cycles = 2 } ); var time_block_1 = session.get_nth_time_block (0); var time_block_2 = session.get_nth_time_block (1); var time_block_3 = session.get_nth_time_block (2); var time_block_4 = session.get_nth_time_block (3); assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_1.end_time), 0.0, 0.0001); assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_2.end_time), 5.0 / 30.0, 0.0001); assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_3.end_time), 5.0 / 55.0, 0.0001); assert_cmpfloat_with_epsilon (session.calculate_break_ratio (time_block_4.end_time), 15.0 / 65.0, 0.0001); } public void test_get_cycles () { var session_0 = new Ft.Session (); assert_cmpuint (session_0.get_cycles ().length (), GLib.CompareOperator.EQ, 0); var session_1 = new Ft.Session.from_template ( Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 1 } ); assert_cmpuint (session_1.get_cycles ().length (), GLib.CompareOperator.EQ, 1); var session_2 = new Ft.Session.from_template ( Ft.SessionTemplate () { pomodoro_duration = 25 * Ft.Interval.MINUTE, short_break_duration = 5 * Ft.Interval.MINUTE, long_break_duration = 15 * Ft.Interval.MINUTE, cycles = 2 } ); assert_cmpuint (session_2.get_cycles ().length (), GLib.CompareOperator.EQ, 2); // When session starts with a break, expect cycles to be 0. var session_3 = new Ft.Session (); session_3.append (new Ft.TimeBlock (Ft.State.SHORT_BREAK)); assert_cmpuint (session_3.get_cycles ().length (), GLib.CompareOperator.EQ, 1); session_3.append (new Ft.TimeBlock (Ft.State.POMODORO)); assert_cmpuint (session_3.get_cycles ().length (), GLib.CompareOperator.EQ, 2); // When pomodoros are after one another, expect cycle to increment; though it's not a true cycle. // In normal case pomodoro would be extended and counted properly. var session_4 = new Ft.Session (); session_4.append (new Ft.TimeBlock (Ft.State.POMODORO)); assert_cmpuint (session_4.get_cycles ().length (), GLib.CompareOperator.EQ, 1); session_4.append (new Ft.TimeBlock (Ft.State.POMODORO)); assert_cmpuint (session_4.get_cycles ().length (), GLib.CompareOperator.EQ, 2); // Treat undefined states same as breaks. var session_5 = new Ft.Session (); session_5.append (new Ft.TimeBlock (Ft.State.STOPPED)); assert_cmpuint (session_5.get_cycles ().length (), GLib.CompareOperator.EQ, 1); session_5.append (new Ft.TimeBlock (Ft.State.POMODORO)); assert_cmpuint (session_5.get_cycles ().length (), GLib.CompareOperator.EQ, 2); // Cycles with uncompleted time-blocks still should be included. var session_6 = new Ft.Session (); var uncompleted_time_block = new Ft.TimeBlock (Ft.State.POMODORO); session_6.append (uncompleted_time_block); session_6.set_time_block_status (uncompleted_time_block, Ft.TimeBlockStatus.UNCOMPLETED); assert_cmpuint (session_6.get_cycles ().length (), GLib.CompareOperator.EQ, 1); session_6.append (new Ft.TimeBlock (Ft.State.POMODORO)); assert_cmpuint (session_6.get_cycles ().length (), GLib.CompareOperator.EQ, 2); } /** * Cycles do not own time-blocks per say. Ensure there is no segfault after removing * time-block from a session. */ public void test_get_cycles__remove_time_block () { var session = new Ft.Session.from_template (this.session_template); var cycle = session.get_cycles ().first ().data; weak Ft.TimeBlock time_block = session.get_first_time_block (); assert_true (cycle.contains (time_block)); session.remove (time_block); assert_true (cycle.contains (time_block)); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.SessionTemplateTest (), new Tests.SessionTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-stats-manager.vala000066400000000000000000002277701520625676500244300ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class BaseStatsManagerTest : Tests.MainLoopTestSuite { protected Ft.Timer? timer; protected Ft.SessionManager? session_manager; protected Ft.StatsManager? stats_manager; protected Ft.TimezoneHistory? timezone_history; protected Gom.Repository? repository; public override void setup () { base.setup (); Ft.Database.open (); var settings = Ft.get_settings (); settings.set_uint ("pomodoro-duration", 1500); settings.set_uint ("short-break-duration", 300); settings.set_uint ("long-break-duration", 900); settings.set_uint ("cycles", 4); settings.set_boolean ("confirm-starting-break", false); settings.set_boolean ("confirm-starting-pomodoro", false); this.repository = Ft.Database.get_repository (); this.timezone_history = new Ft.TimezoneHistory (); this.timer = new Ft.Timer (); Ft.Timer.set_default (this.timer); this.session_manager = new Ft.SessionManager.with_timer (this.timer); Ft.SessionManager.set_default (this.session_manager); this.stats_manager = new Ft.StatsManager (); assert (!this.stats_manager.get_data ("teardown")); } public override void teardown () { this.timezone_history.clear_cache (); this.stats_manager.set_data ("teardown", true); this.stats_manager = null; this.session_manager = null; this.timer = null; this.timezone_history = null; this.repository = null; Ft.SessionManager.set_default (null); Ft.Timer.set_default (null); var settings = Ft.get_settings (); settings.revert (); Ft.Database.close (); base.teardown (); } protected Gom.ResourceGroup fetch (string date, string category) throws GLib.Error { var date_value = GLib.Value (typeof (string)); date_value.set_string (date); var category_value = GLib.Value (typeof (string)); category_value.set_string (category); var date_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "date", date_value); var category_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "category", category_value); var filter = new Gom.Filter.and (date_filter, category_filter); return this.repository.find_sync (typeof (Ft.StatsEntry), filter); } protected Ft.AggregatedStatsEntry? fetch_aggregated (string date, string category) throws GLib.Error { var date_value = GLib.Value (typeof (string)); date_value.set_string (date); var category_value = GLib.Value (typeof (string)); category_value.set_string (category); var date_filter = new Gom.Filter.eq ( typeof (Ft.AggregatedStatsEntry), "date", date_value); var category_filter = new Gom.Filter.eq ( typeof (Ft.AggregatedStatsEntry), "category", category_value); var filter = new Gom.Filter.and (date_filter, category_filter); return (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), filter); } protected uint count (string date, string category) throws GLib.Error { try { var results = this.fetch (date, category); return results.count; } catch (GLib.Error error) { throw error; } } protected uint count_aggregated (string date, string category) throws GLib.Error { try { var aggregated_entry = this.fetch_aggregated (date, category); if (aggregated_entry == null) { return 0U; } assert_true (aggregated_entry.count >= 0); return (uint) aggregated_entry.count; } catch (GLib.Error error) { throw error; } } protected int64 sum (string date, string category) throws GLib.Error { try { var results = this.fetch (date, category); var duration = (int64) 0; results.fetch_sync (0U, results.count); for (var index = 0U; index < results.count; index++) { var entry = (Ft.StatsEntry?) results.get_index (index); duration += entry.duration; } return duration; } catch (GLib.Error error) { throw error; } } protected int64 sum_aggregated (string date, string category) throws GLib.Error { try { var aggregated_entry = this.fetch_aggregated (date, category); return aggregated_entry.duration; } catch (GLib.Error error) { throw error; } } } public class StatsManagerTest : BaseStatsManagerTest { private GLib.TimeZone? new_york_timezone; private GLib.TimeZone? london_timezone; private GLib.TimeZone? los_angeles_timezone; public StatsManagerTest () { this.add_test ("track", this.test_track); this.add_test ("track__update", this.test_track__update); this.add_test ("track__many", this.test_track__many); this.add_test ("track__duplicate_1", this.test_track__duplicate_1); this.add_test ("track__duplicate_2", this.test_track__duplicate_2); this.add_test ("track_time_block__pomodoro_1", this.test_track_time_block__pomodoro_1); this.add_test ("track_time_block__pomodoro_2", this.test_track_time_block__pomodoro_2); this.add_test ("track_time_block__break", this.test_track_time_block__break); this.add_test ("track_time_block__skip_unfinished", this.test_track_time_block__skip_unfinished); this.add_test ("track_time_block__update", this.test_track_time_block__update); this.add_test ("track_gap", this.test_track_gap); this.add_test ("track_gap__skip_unfinished", this.test_track_gap__skip_unfinished); this.add_test ("track_gap__skip_non_interruption", this.test_track_gap__skip_non_interruption); this.add_test ("gap__update", this.test_gap__update); this.add_test ("midnight_split__before_true_midnight", this.test_midnight_split__before_true_midnight); this.add_test ("midnight_split__after_true_midnight", this.test_midnight_split__after_true_midnight); this.add_test ("midnight_split__multiple_days", this.test_midnight_split__multiple_days); this.add_test ("timezone_change__forward", this.test_timezone_change__forward); this.add_test ("timezone_change__backward", this.test_timezone_change__backward); this.add_test ("dst_change__forward", this.test_dst_change__forward); this.add_test ("dst_change__backward", this.test_dst_change__backward); } public override void setup () { base.setup (); // Sat January 01 2000 08:00:00 UTC Ft.Timestamp.freeze_to (Ft.Timestamp.from_seconds_uint (946713600)); try { this.new_york_timezone = new GLib.TimeZone.identifier ("America/New_York"); // 3 AM this.london_timezone = new GLib.TimeZone.identifier ("Europe/London"); // 8 AM this.los_angeles_timezone = new GLib.TimeZone.identifier ("America/Los_Angeles"); // 0 AM } catch (GLib.Error error) { assert_no_error (error); } this.timezone_history.insert (Ft.Timestamp.peek (), this.new_york_timezone); } private void run_flush () { this.stats_manager.flush.begin ( (obj, res) => { this.stats_manager.flush.end (res); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); } public void test_track () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var source_id = (int64) 12345; this.stats_manager.track ("test", timestamp, Ft.Interval.MINUTE, source_id); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), null); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); // Expect aggregated entries to be up to date results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.count), new GLib.Variant.int64 (1)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track__many () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); timestamp += Ft.Interval.HOUR; this.stats_manager.track ("other", timestamp, 2 * Ft.Interval.HOUR, (int64) 1); // extra entry this.stats_manager.track ("test", timestamp, Ft.Interval.MINUTE, (int64) 2); timestamp += Ft.Interval.MINUTE; this.stats_manager.track ("test", timestamp, 2 * Ft.Interval.MINUTE, (int64) 3); timestamp += Ft.Interval.MINUTE; this.stats_manager.track ("test", timestamp, Ft.Interval.HOUR, (int64) 4); timestamp += 24 * Ft.Interval.HOUR; this.stats_manager.track ("test", timestamp, 2 * Ft.Interval.HOUR, (int64) 5); // extra entry this.run_flush (); try { assert_cmpvariant ( new GLib.Variant.int64 (this.sum ("2000-01-01", "test")), new GLib.Variant.int64 (Ft.Interval.HOUR + 3 * Ft.Interval.MINUTE)); assert_cmpuint ( this.count ("2000-01-01", "test"), GLib.CompareOperator.EQ, 3U); assert_cmpvariant ( new GLib.Variant.int64 (this.sum_aggregated ("2000-01-01", "test")), new GLib.Variant.int64 (Ft.Interval.HOUR + 3 * Ft.Interval.MINUTE)); assert_cmpuint ( this.count_aggregated ("2000-01-01", "test"), GLib.CompareOperator.EQ, 3U); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track__update () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); this.stats_manager.track ( // entry to keep "test", timestamp - 1, Ft.Interval.MINUTE); this.stats_manager.track ( "test", timestamp, 5 * Ft.Interval.MINUTE); this.run_flush (); this.stats_manager.track ( "test", timestamp, 6 * Ft.Interval.MINUTE); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2); var time_value = GLib.Value (typeof (int64)); time_value.set_int64 (timestamp); var time_filter = new Gom.Filter.eq ( typeof (Ft.StatsEntry), "time", time_value); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), time_filter); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "test"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE)); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "test"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 ((1 + 6) * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track__duplicate_1 () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); var source_id = 2; this.stats_manager.track ( "test", timestamp, Ft.Interval.MINUTE, source_id); this.stats_manager.track ( "test", timestamp, Ft.Interval.MINUTE, source_id); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track__duplicate_2 () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); var source_id = 2; this.stats_manager.track ( "test", timestamp, Ft.Interval.MINUTE, source_id); this.run_flush (); this.stats_manager.track ( "test", timestamp, Ft.Interval.MINUTE, source_id); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track_time_block__pomodoro_1 () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (timestamp + 1 * Ft.Interval.MINUTE, timestamp + 3 * Ft.Interval.MINUTE); time_block.add_gap (gap); var session = new Ft.Session (); session.append (time_block); this.stats_manager.track_time_block (time_block); this.run_flush (); // Expect two entries for each continuous segment of pomodoro separated by a gap try { Gom.ResourceGroup results; var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.StatsEntry), "time", Gom.SortingMode.ASCENDING); results = this.repository.find_sorted_sync (typeof (Ft.StatsEntry), null, sorting); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.time), new GLib.Variant.int64 (time_block.start_time)); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (1 * Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.time), new GLib.Variant.int64 (gap.end_time)); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR + 3 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE)); // Expect aggregated entries to be up to date results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpstr ( aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.count), new GLib.Variant.int64 (1)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track_time_block__pomodoro_2 () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_1.set_time_range (timestamp, timestamp + 5 * Ft.Interval.MINUTE); time_block_1.set_status (Ft.TimeBlockStatus.COMPLETED); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (timestamp + 1 * Ft.Interval.MINUTE, timestamp + 3 * Ft.Interval.MINUTE); time_block_1.add_gap (gap); // Second pomodoro later the same day var timestamp_2 = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 9, 0, 0)); var time_block_2 = new Ft.TimeBlock (Ft.State.POMODORO); time_block_2.set_time_range (timestamp_2, timestamp_2 + 10 * Ft.Interval.MINUTE); time_block_2.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block_1); session.append (time_block_2); this.stats_manager.track_time_block (time_block_1); this.stats_manager.track_time_block (time_block_2); this.run_flush (); try { var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpstr ( aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (13 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.count), new GLib.Variant.int64 (2)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Expect various brake types to be treated as just "break". */ public void test_track_time_block__break () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.SHORT_BREAK); time_block.set_time_range (timestamp, timestamp + 5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (timestamp + 1 * Ft.Interval.MINUTE, timestamp + 3 * Ft.Interval.MINUTE); time_block.add_gap (gap); var session = new Ft.Session (); session.append (time_block); this.stats_manager.track_time_block (time_block); this.run_flush (); // Expect two entries for each continuous segment of break separated by a gap try { Gom.ResourceGroup results; var sorting = (Gom.Sorting) GLib.Object.@new (typeof (Gom.Sorting)); sorting.add (typeof (Ft.StatsEntry), "time", Gom.SortingMode.ASCENDING); results = this.repository.find_sorted_sync (typeof (Ft.StatsEntry), null, sorting); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "break"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.time), new GLib.Variant.int64 (time_block.start_time)); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (1 * Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "break"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.time), new GLib.Variant.int64 (gap.end_time)); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR + 3 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE)); // Expect aggregated entries to be up to date results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "break"); assert_cmpstr ( aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Only track interruptions that have been finished. */ public void test_track_time_block__skip_unfinished () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock.with_start_time (timestamp, Ft.State.POMODORO); time_block.end_time = Ft.Timestamp.UNDEFINED; var session = new Ft.Session (); session.append (time_block); this.stats_manager.track_time_block (time_block); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0U); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track_time_block__update () { Ft.StatsEntry? original_stats_entry = null; var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 5 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block); this.stats_manager.track_time_block (time_block); this.run_flush (); try { original_stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), null); } catch (GLib.Error error) { assert_no_error (error); } // Edit pomodoro time_block.set_time_range (timestamp, timestamp + 9 * Ft.Interval.MINUTE); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (timestamp + 4 * Ft.Interval.MINUTE, timestamp + 6 * Ft.Interval.MINUTE); time_block.add_gap (gap); this.stats_manager.track_time_block (time_block); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.source_id), new GLib.Variant.int64 (original_stats_entry.source_id)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.id), new GLib.Variant.int64 (original_stats_entry.id)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.time), new GLib.Variant.int64 (time_block.start_time)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.source_id), new GLib.Variant.int64 (original_stats_entry.source_id)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR + 6 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.time), new GLib.Variant.int64 (gap.end_time)); results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (7 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.count), new GLib.Variant.int64 (1)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_track_gap () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block); var gap_1 = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap_1.set_time_range (time_block.start_time + 4 * Ft.Interval.MINUTE, time_block.start_time + 5 * Ft.Interval.MINUTE); time_block.add_gap (gap_1); var gap_2 = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap_2.set_time_range (time_block.start_time + 6 * Ft.Interval.MINUTE, time_block.start_time + 10 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); this.stats_manager.track_gap (gap_1); this.stats_manager.track_gap (gap_2); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR + 4 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (7 * Ft.Interval.HOUR + 6 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpstr ( aggregated_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.count), new GLib.Variant.int64 (2)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Only track interruptions that have been finished. */ public void test_track_gap__skip_unfinished () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block); var gap = new Ft.Gap.with_start_time ( time_block.start_time + 4 * Ft.Interval.MINUTE, Ft.GapFlags.INTERRUPTION); time_block.add_gap (gap); this.stats_manager.track_gap (gap); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0U); } catch (GLib.Error error) { assert_no_error (error); } } /** * Only track interruptions. */ public void test_track_gap__skip_non_interruption () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 7, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.BREAK); time_block.set_time_range (timestamp, timestamp + 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block); var gap = new Ft.Gap (); gap.set_time_range (time_block.start_time + 4 * Ft.Interval.MINUTE, time_block.start_time + 5 * Ft.Interval.MINUTE); time_block.add_gap (gap); this.stats_manager.track_gap (gap); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 0U); } catch (GLib.Error error) { assert_no_error (error); } } public void test_gap__update () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (timestamp, timestamp + 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.COMPLETED); var session = new Ft.Session (); session.append (time_block); var gap = new Ft.Gap (Ft.GapFlags.INTERRUPTION); gap.set_time_range (time_block.start_time + 4 * Ft.Interval.MINUTE, time_block.start_time + 5 * Ft.Interval.MINUTE); time_block.add_gap (gap); this.stats_manager.track_gap (gap); this.run_flush (); gap.set_time_range (time_block.start_time + 4 * Ft.Interval.MINUTE, time_block.start_time + 6 * Ft.Interval.MINUTE); this.stats_manager.track_gap (gap); this.run_flush (); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), null); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE)); var aggregated_entry = (Ft.AggregatedStatsEntry?) this.repository.find_one_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpstr ( aggregated_entry.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpvariant ( new GLib.Variant.int64 (aggregated_entry.duration), new GLib.Variant.int64 (2 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Scenario for 23:00 - 04:30 * Expect that time before 4 AM will be attributed to the previous day. */ public void test_midnight_split__before_true_midnight () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 23, 0, 0)); this.stats_manager.track ( "test", timestamp, 5 * Ft.Interval.HOUR + 30 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (23 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (5 * Ft.Interval.HOUR)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-02"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (Ft.StatsManager.MIDNIGHT_OFFSET)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (30 * Ft.Interval.MINUTE)); // Expect an aggregated entry for each day results = this.repository.find_sync ( typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var agg_stats_entry_1 = (Ft.AggregatedStatsEntry?) results.get_index (0U); assert_cmpstr ( agg_stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( agg_stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (agg_stats_entry_1.duration), new GLib.Variant.int64 (5 * Ft.Interval.HOUR)); var agg_stats_entry_2 = (Ft.AggregatedStatsEntry?) results.get_index (1U); assert_cmpstr ( agg_stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( agg_stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-02"); assert_cmpvariant ( new GLib.Variant.int64 (agg_stats_entry_2.duration), new GLib.Variant.int64 (30 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Scenario for 03:00 - 04:30 * Expect that time before 4 AM will be attributed to the previous day. */ public void test_midnight_split__after_true_midnight () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 3, 0, 0)); this.stats_manager.track ( "test", timestamp, 90 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "1999-12-31"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 ((24 + 3) * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.HOUR)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (Ft.StatsManager.MIDNIGHT_OFFSET)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (30 * Ft.Interval.MINUTE)); // Expect an aggregated entry for each day results = this.repository.find_sync (typeof (Ft.AggregatedStatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var agg_stats_entry_1 = (Ft.AggregatedStatsEntry?) results.get_index (0U); assert_cmpstr ( agg_stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( agg_stats_entry_1.date, GLib.CompareOperator.EQ, "1999-12-31"); assert_cmpvariant ( new GLib.Variant.int64 (agg_stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.HOUR)); var agg_stats_entry_2 = (Ft.AggregatedStatsEntry?) results.get_index (1U); assert_cmpstr ( agg_stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( agg_stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (agg_stats_entry_2.duration), new GLib.Variant.int64 (30 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Scenario for Monday 09:00 - Friday 17:00 */ public void test_midnight_split__multiple_days () { var start_time = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 3, 9, 0, 0)); var end_time = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 7, 17, 0, 0)); this.stats_manager.track ("test", start_time, end_time - start_time); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 5U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-03"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (9 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 ((24 - 9) * Ft.Interval.HOUR + Ft.StatsManager.MIDNIGHT_OFFSET)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-04"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 (Ft.StatsManager.MIDNIGHT_OFFSET)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (24 * Ft.Interval.HOUR)); var stats_entry_5 = (Ft.StatsEntry?) results.get_index (4U); assert_cmpstr ( stats_entry_5.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_5.date, GLib.CompareOperator.EQ, "2000-01-07"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_5.offset), new GLib.Variant.int64 (Ft.StatsManager.MIDNIGHT_OFFSET)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_5.duration), new GLib.Variant.int64 (17 * Ft.Interval.HOUR - Ft.StatsManager.MIDNIGHT_OFFSET)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Jump from America/New_York 12:01 to Europe/London 17:01 */ public void test_timezone_change__forward () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); this.timezone_history.insert (timestamp + Ft.Interval.MINUTE, this.london_timezone); this.stats_manager.track ( "test", timestamp, 5 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (12 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 ((12 + 5) * Ft.Interval.HOUR + Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Jump from America/New_York 12:01 to America/Los_Angeles 09:01 */ public void test_timezone_change__backward () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); this.timezone_history.insert (timestamp + Ft.Interval.MINUTE, this.los_angeles_timezone); this.stats_manager.track ( "test", timestamp, 5 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 (12 * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 ((12 - 3) * Ft.Interval.HOUR + Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Detect DST switch from 2:00 AM EST to 3:00 AM EDT */ public void test_dst_change__forward () { var dst_switch_time = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 4, 2, 2, 0, 0)); this.stats_manager.track ( "test", dst_switch_time - Ft.Interval.MINUTE, 5 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2000-04-01"); // adjusted to virtual midnight assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 ((24 + 2) * Ft.Interval.HOUR - Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2000-04-01"); // adjusted to virtual midnight assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 ((24 + 3) * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } /** * Detect DST switch from 2:00 AM EDT to 1:00 AM EST */ public void test_dst_change__backward () { var dst_switch_time = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2001, 10, 28, 0, 59, 59)) + Ft.Interval.HOUR + Ft.Interval.SECOND; var current_timezone = this.timezone_history.search (dst_switch_time); assert_cmpstr (current_timezone.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); this.stats_manager.track ( "test", dst_switch_time - Ft.Interval.MINUTE, 5 * Ft.Interval.MINUTE); this.run_flush (); try { Gom.ResourceGroup results; results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 2U); results.fetch_sync (0U, results.count); var stats_entry_1 = (Ft.StatsEntry?) results.get_index (0U); assert_cmpstr ( stats_entry_1.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_1.date, GLib.CompareOperator.EQ, "2001-10-27"); // adjusted to virtual midnight assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.offset), new GLib.Variant.int64 ((24 + 2) * Ft.Interval.HOUR - Ft.Interval.MINUTE)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_1.duration), new GLib.Variant.int64 (Ft.Interval.MINUTE)); var stats_entry_2 = (Ft.StatsEntry?) results.get_index (1U); assert_cmpstr ( stats_entry_2.category, GLib.CompareOperator.EQ, "test"); assert_cmpstr ( stats_entry_2.date, GLib.CompareOperator.EQ, "2001-10-27"); // adjusted to virtual midnight assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.offset), new GLib.Variant.int64 ((24 + 1) * Ft.Interval.HOUR)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry_2.duration), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE)); } catch (GLib.Error error) { assert_no_error (error); } } } public class StatsManagerSessionManagerTest : BaseStatsManagerTest { private GLib.TimeZone? new_york_timezone; public StatsManagerSessionManagerTest () { this.add_test ("save__pomodoro", this.test_save__pomodoro); this.add_test ("save__break", this.test_save__break); this.add_test ("save__interruption", this.test_save__interruption); this.add_test ("save__pomodoro_with_interruptions", this.test_save__pomodoro_with_interruptions); } public override void setup () { base.setup (); // Sat Jan 01 2000 08:00:00 UTC+0000 Ft.Timestamp.freeze_to (Ft.Timestamp.from_seconds_uint (946713600)); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); try { this.new_york_timezone = new GLib.TimeZone.identifier ("America/New_York"); // 3 AM } catch (GLib.Error error) { assert_no_error (error); } this.timezone_history.insert (Ft.Timestamp.peek (), this.new_york_timezone); } private void run_save () { this.session_manager.save.begin ( (obj, res) => { assert_true (this.session_manager.save.end (res)); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); this.stats_manager.flush.begin ( (obj, res) => { this.stats_manager.flush.end (res); this.quit_main_loop (); }); assert_true (this.run_main_loop ()); } public void test_save__pomodoro () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); Ft.Timestamp.freeze_to (timestamp); this.session_manager.advance_to_state (Ft.State.POMODORO); this.session_manager.advance (this.session_manager.current_time_block.end_time); var time_block = this.session_manager.current_session.get_first_time_block (); var time_block_saved_emitted = 0U; this.session_manager.time_block_saved.connect ( () => { time_block_saved_emitted++; }); this.run_save (); assert_cmpuint (time_block_saved_emitted, GLib.CompareOperator.EQ, 2U); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), null); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "pomodoro"); assert_cmpstr ( stats_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (time_block.duration)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.offset), new GLib.Variant.int64 (12 * Ft.Interval.HOUR)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__break () { var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); Ft.Timestamp.freeze_to (timestamp); this.session_manager.advance_to_state (Ft.State.SHORT_BREAK); this.session_manager.advance (this.session_manager.current_time_block.end_time); var time_block = this.session_manager.current_session.get_first_time_block (); var time_block_saved_emitted = 0U; this.session_manager.time_block_saved.connect ( (time_block, time_block_entry) => { time_block_saved_emitted++; }); this.run_save (); assert_cmpuint (time_block_saved_emitted, GLib.CompareOperator.EQ, 2U); try { var results = this.repository.find_sync (typeof (Ft.StatsEntry), null); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), null); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "break"); assert_cmpstr ( stats_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (time_block.duration)); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.offset), new GLib.Variant.int64 (12 * Ft.Interval.HOUR)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__interruption () { var timer_action_group = new Ft.TimerActionGroup.with_timer (this.timer); var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); var time_block_saved_emitted = 0U; var gap_saved_emitted = 0U; this.session_manager.time_block_saved.connect ( (time_block, time_block_entry) => { time_block_saved_emitted++; }); this.session_manager.gap_saved.connect ( (gap, gap_entry) => { gap_saved_emitted++; }); Ft.Timestamp.freeze_to (timestamp); timer_action_group.activate_action ("start", null); Ft.Timestamp.freeze_to (timestamp + 5 * Ft.Interval.MINUTE); timer_action_group.activate_action ("pause", null); Ft.Timestamp.freeze_to (timestamp + 6 * Ft.Interval.MINUTE); timer_action_group.activate_action ("resume", null); this.run_save (); assert_cmpuint (time_block_saved_emitted, GLib.CompareOperator.EQ, 1U); assert_cmpuint (gap_saved_emitted, GLib.CompareOperator.EQ, 1U); var time_block = this.session_manager.current_time_block; var gap = time_block.get_last_gap (); try { var category_value = GLib.Value (typeof (string)); category_value.set_string ("pomodoro"); var category_filter = new Gom.Filter.neq ( typeof (Ft.StatsEntry), "category", category_value); var results = this.repository.find_sync (typeof (Ft.StatsEntry), category_filter); assert_cmpuint (results.count, GLib.CompareOperator.EQ, 1U); var stats_entry = (Ft.StatsEntry?) this.repository.find_one_sync ( typeof (Ft.StatsEntry), category_filter); assert_cmpstr ( stats_entry.category, GLib.CompareOperator.EQ, "interruption"); assert_cmpstr ( stats_entry.date, GLib.CompareOperator.EQ, "2000-01-01"); assert_cmpvariant ( new GLib.Variant.int64 (stats_entry.duration), new GLib.Variant.int64 (gap.duration)); } catch (GLib.Error error) { assert_no_error (error); } } public void test_save__pomodoro_with_interruptions () { var timer_action_group = new Ft.TimerActionGroup.with_timer (this.timer); var timestamp = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 1, 1, 12, 0, 0)); Ft.Timestamp.freeze_to (timestamp); this.session_manager.advance_to_state (Ft.State.POMODORO); // First interruption: 12:02 - 12:03 Ft.Timestamp.freeze_to (timestamp + 2 * Ft.Interval.MINUTE); timer_action_group.activate_action ("pause", null); Ft.Timestamp.freeze_to (timestamp + 3 * Ft.Interval.MINUTE); timer_action_group.activate_action ("resume", null); // Second interruption: 12:10 - 12:11 Ft.Timestamp.freeze_to (timestamp + 10 * Ft.Interval.MINUTE); timer_action_group.activate_action ("pause", null); Ft.Timestamp.freeze_to (timestamp + 11 * Ft.Interval.MINUTE); timer_action_group.activate_action ("resume", null); // Mark the time-block end at its scheduled end time this.session_manager.advance (this.session_manager.current_time_block.end_time); this.run_save (); try { assert_cmpuint (this.count ("2000-01-01", "pomodoro"), GLib.CompareOperator.EQ, 3U); assert_cmpuint (this.count ("2000-01-01", "interruption"), GLib.CompareOperator.EQ, 2U); assert_cmpuint (this.count_aggregated ("2000-01-01", "pomodoro"), GLib.CompareOperator.EQ, 1U); assert_cmpuint (this.count_aggregated ("2000-01-01", "interruption"), GLib.CompareOperator.EQ, 2U); } catch (GLib.Error error) { assert_no_error (error); } } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.StatsManagerTest (), new Tests.StatsManagerSessionManagerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-time-block.vala000066400000000000000000001231431520625676500236750ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class TimeBlockTest : Tests.TestSuite { public TimeBlockTest () { this.add_test ("new__undefined", this.test_new__undefined); this.add_test ("new__pomodoro", this.test_new__pomodoro); this.add_test ("new__short_break", this.test_new__short_break); this.add_test ("new__long_break", this.test_new__long_break); this.add_test ("set_session", this.test_set_session); this.add_test ("move_by__without_gaps", this.test_move_by__without_gaps); this.add_test ("move_by__with_gaps", this.test_move_by__with_gaps); this.add_test ("move_to", this.test_move_to); this.add_test ("add_gap", this.test_add_gap); this.add_test ("remove_gap", this.test_remove_gap); this.add_test ("normalize_gaps__invalid", this.test_normalize_gaps__invalid); this.add_test ("normalize_gaps__overlap", this.test_normalize_gaps__overlap); this.add_test ("normalize_gaps__unfinished", this.test_normalize_gaps__unfinished); this.add_test ("calculate_elapsed__without_gaps", this.test_calculate_elapsed__without_gaps); this.add_test ("calculate_elapsed__with_gaps", this.test_calculate_elapsed__with_gaps); this.add_test ("calculate_elapsed__with_ongoing_gap", this.test_calculate_elapsed__with_ongoing_gap); this.add_test ("calculate_elapsed__with_gaps_overlapping", this.test_calculate_elapsed__with_gaps_overlapping); this.add_test ("calculate_remaining__without_gaps", this.test_calculate_remaining__without_gaps); this.add_test ("calculate_remaining__with_gaps", this.test_calculate_remaining__with_gaps); this.add_test ("calculate_remaining__with_ongoing_gap", this.test_calculate_remaining__with_ongoing_gap); this.add_test ("calculate_remaining__with_gaps_overlapping", this.test_calculate_remaining__with_gaps_overlapping); this.add_test ("calculate_progress__without_gaps", this.test_calculate_progress__without_gaps); this.add_test ("calculate_progress__with_gaps", this.test_calculate_progress__with_gaps); this.add_test ("calculate_progress__with_ongoing_gap", this.test_calculate_progress__with_ongoing_gap); // this.add_test ("state", this.test_state); // this.add_test ("start_time", this.test_start_time); // this.add_test ("end_time", this.test_end_time); // this.add_test ("duration", this.test_duration); // this.add_test ("changed_signal", this.test_changed_signal); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); // var settings = Ft.get_settings (); // settings.set_uint ("pomodoro-duration", POMODORO_DURATION); // settings.set_uint ("short-break-duration", SHORT_BREAK_DURATION); // settings.set_uint ("long-break-duration", LONG_BREAK_DURATION); // settings.set_uint ("cycles", CYCLES); } public override void teardown () { Ft.Timestamp.thaw (); // var settings = Ft.get_settings (); // settings.revert (); } /* * Tests for constructors */ public void test_new__undefined () { var state = Ft.State.STOPPED; var time_block = new Ft.TimeBlock (state); assert_true (time_block.state == state); assert_true (Ft.Timestamp.is_undefined (time_block.start_time)); assert_true (Ft.Timestamp.is_undefined (time_block.end_time)); } public void test_new__pomodoro () { var state = Ft.State.POMODORO; var time_block = new Ft.TimeBlock (state); assert_true (time_block.state == state); assert_true (Ft.Timestamp.is_undefined (time_block.start_time)); assert_true (Ft.Timestamp.is_undefined (time_block.end_time)); } public void test_new__short_break () { var state = Ft.State.SHORT_BREAK; var time_block = new Ft.TimeBlock (state); assert_true (time_block.state == state); assert_true (Ft.Timestamp.is_undefined (time_block.start_time)); assert_true (Ft.Timestamp.is_undefined (time_block.end_time)); } public void test_new__long_break () { var state = Ft.State.LONG_BREAK; var time_block = new Ft.TimeBlock (state); assert_true (time_block.state == state); assert_true (Ft.Timestamp.is_undefined (time_block.start_time)); assert_true (Ft.Timestamp.is_undefined (time_block.end_time)); } /* * Tests for properties */ public void test_set_session () { var time_block = new Ft.TimeBlock (Ft.State.POMODORO); var notify_session_emitted = 0; time_block.notify["session"].connect (() => { notify_session_emitted++; }); var session_1 = new Ft.Session (); time_block.session = session_1; assert_true (time_block.session == session_1); assert_true (notify_session_emitted == 1); time_block.session = session_1; assert_true (time_block.session == session_1); assert_true (notify_session_emitted == 1); // unchanged var session_2 = new Ft.Session (); time_block.session = session_2; assert_true (time_block.session == session_2); assert_true (notify_session_emitted == 2); } public void test_state () { var time_block_1 = new Ft.TimeBlock (Ft.State.POMODORO); assert_true (time_block_1.state == Ft.State.POMODORO); var time_block_2 = new Ft.TimeBlock (Ft.State.BREAK); assert_true (time_block_2.state == Ft.State.BREAK); var time_block_3 = new Ft.TimeBlock (Ft.State.STOPPED); assert_true (time_block_3.state == Ft.State.STOPPED); } // public void test_start_time () // { // } // public void test_end_time () // { // } // public void test_duration () // { // } // public void test_session () // { // } /* * Tests for methods */ public void test_move_by__without_gaps () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (); var changed_emitted = 0; time_block.changed.connect (() => { changed_emitted++; }); time_block.set_time_range (Ft.Timestamp.UNDEFINED, now); time_block.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); time_block.set_time_range (now, Ft.Timestamp.UNDEFINED); time_block.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 4); time_block.set_time_range (now, now + Ft.Interval.MINUTE); time_block.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now + 2 * Ft.Interval.MINUTE) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 6); time_block.move_by (0); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 6); } public void test_move_by__with_gaps () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (); time_block.set_time_range (now, now + 5 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (now + 0 * Ft.Interval.SECOND, now + 5 * Ft.Interval.SECOND); time_block.add_gap (gap_1); var gap_2 = new Ft.Gap (); gap_2.set_time_range (now + 10 * Ft.Interval.SECOND, now + 20 * Ft.Interval.SECOND); time_block.add_gap (gap_2); time_block.move_by (Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (gap_1.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE + 0 * Ft.Interval.SECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_1.end_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE + 5 * Ft.Interval.SECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_2.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE + 10 * Ft.Interval.SECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (gap_2.end_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE + 20 * Ft.Interval.SECOND) ); } public void test_move_to () { var now = Ft.Timestamp.advance (0); var time_block = new Ft.TimeBlock (); var changed_emitted = 0; time_block.changed.connect (() => { changed_emitted++; }); // Move +1 minute, while range is not defined. time_block.set_time_range (Ft.Timestamp.UNDEFINED, now); time_block.move_to (now + Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); // Move +1 minute, while range is not defined. time_block.set_time_range (now, Ft.Timestamp.UNDEFINED); time_block.move_to (now + Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 3); // Move +1 minute. time_block.set_time_range (now, now + Ft.Interval.MINUTE); time_block.move_to (now + Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now + 2 * Ft.Interval.MINUTE) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 5); // Move -1 minute. time_block.set_time_range (now + Ft.Interval.MINUTE, now + 2 * Ft.Interval.MINUTE); time_block.move_to (now); assert_cmpvariant ( new GLib.Variant.int64 (time_block.start_time), new GLib.Variant.int64 (now) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.end_time), new GLib.Variant.int64 (now + Ft.Interval.MINUTE) ); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 6); // Move 0 minutes. time_block.move_to (time_block.start_time); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 6); } // public void test_has_started () // { // } // public void test_has_ended () // { // } public void test_add_gap () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (); time_block.set_time_range (now + Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); var changed_emitted = 0; time_block.changed.connect (() => { changed_emitted++; }); var gap_1 = new Ft.Gap (); gap_1.set_time_range (time_block.start_time + 5 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); // Expect that `Gap.time_block` is set and that `changed` signal is emitted. time_block.add_gap (gap_1); assert_true (gap_1.time_block == time_block); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); time_block.add_gap (gap_1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); // no change gap_1.end_time = time_block.start_time + 10 * Ft.Interval.MINUTE; assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); // Expect add_gap to keep gaps in order. var gap_2 = new Ft.Gap (); gap_2.set_time_range (time_block.start_time, time_block.start_time + Ft.Interval.MINUTE); time_block.add_gap (gap_2); assert_true (time_block.get_last_gap () == gap_1); var gap_3 = new Ft.Gap (); gap_3.set_time_range (time_block.start_time + 20 * Ft.Interval.MINUTE, time_block.start_time + 25 * Ft.Interval.MINUTE); time_block.add_gap (gap_3); assert_true (time_block.get_last_gap () == gap_3); } public void test_normalize_gaps__invalid () { var time_block = new Ft.TimeBlock (); var gap_valid = new Ft.Gap (); gap_valid.set_time_range (10 * Ft.Interval.MINUTE, 20 * Ft.Interval.MINUTE); var gap_invalid_undefined_start = new Ft.Gap (); gap_invalid_undefined_start.set_time_range (Ft.Timestamp.UNDEFINED, 12 * Ft.Interval.MINUTE); var gap_invalid_end_before_start = new Ft.Gap (); gap_invalid_end_before_start.set_time_range (25 * Ft.Interval.MINUTE, 24 * Ft.Interval.MINUTE); time_block.add_gap (gap_valid); time_block.add_gap (gap_invalid_undefined_start); time_block.add_gap (gap_invalid_end_before_start); time_block.normalize_gaps (); // Expect only the valid gap to remain and to be unchanged var gaps_count = 0; time_block.foreach_gap ( (gap) => { if (gaps_count == 0) { assert_true (gap == gap_valid); assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (10 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); } gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); } public void test_normalize_gaps__overlap () { int gaps_count; // unowned Ft.Gap? gap; // Case 1: Two finite gaps overlapping. Expect them to be merged into one // by extending the later gap backwards - preserving total duration. var time_block_1 = new Ft.TimeBlock (); var gap_1 = new Ft.Gap (); gap_1.set_time_range (10 * Ft.Interval.MINUTE, 20 * Ft.Interval.MINUTE); var gap_2 = new Ft.Gap (); gap_2.set_time_range (18 * Ft.Interval.MINUTE, 25 * Ft.Interval.MINUTE); time_block_1.add_gap (gap_1); time_block_1.add_gap (gap_2); time_block_1.normalize_gaps (); // Expect one gap to remain with start moved to 8 and end 25 gaps_count = 0; time_block_1.foreach_gap ( (gap) => { if (gaps_count == 0) { assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (8 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); } gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 1); // Case 2: Overlap with an ongoing (unfinished) next gap. Expect previous gap // to be shifted back. var time_block_2 = new Ft.TimeBlock (); var gap_3 = new Ft.Gap (); gap_3.set_time_range (10 * Ft.Interval.MINUTE, 20 * Ft.Interval.MINUTE); var gap_4 = new Ft.Gap (); gap_4.set_time_range (18 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block_2.add_gap (gap_3); time_block_2.add_gap (gap_4); time_block_2.normalize_gaps (); gaps_count = 0; time_block_2.foreach_gap ( (gap) => { if (gaps_count == 0) { assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (8 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (18 * Ft.Interval.MINUTE) ); } if (gaps_count == 1) { assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (18 * Ft.Interval.MINUTE) ); assert_true (Ft.Timestamp.is_undefined (gap.end_time)); } gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 2); } public void test_normalize_gaps__unfinished () { // Unfinished (ongoing) gap not overlapping should remain unchanged var time_block = new Ft.TimeBlock (); var gap_1 = new Ft.Gap (); gap_1.set_time_range (10 * Ft.Interval.MINUTE, 15 * Ft.Interval.MINUTE); var gap_2 = new Ft.Gap (); gap_2.set_time_range (20 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap_1); time_block.add_gap (gap_2); time_block.normalize_gaps (); // Expect both gaps unchanged var gaps_count = 0; time_block.foreach_gap ( (gap) => { if (gaps_count == 0) { assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (10 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (gap.end_time), new GLib.Variant.int64 (15 * Ft.Interval.MINUTE) ); } if (gaps_count == 1) { assert_cmpvariant ( new GLib.Variant.int64 (gap.start_time), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); assert_true (Ft.Timestamp.is_undefined (gap.end_time)); } gaps_count++; }); assert_cmpuint (gaps_count, GLib.CompareOperator.EQ, 2); } public void test_remove_gap () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (); time_block.set_time_range (now + Ft.Interval.MINUTE, now + 30 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (time_block.start_time + 1 * Ft.Interval.MINUTE, time_block.start_time + 2 * Ft.Interval.MINUTE); time_block.add_gap (gap_1); var gap_2 = new Ft.Gap (); gap_2.set_time_range (time_block.start_time + 3 * Ft.Interval.MINUTE, time_block.start_time + 4 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); var gap_3 = new Ft.Gap (); gap_3.set_time_range (time_block.start_time + 5 * Ft.Interval.MINUTE, time_block.start_time + 6 * Ft.Interval.MINUTE); var changed_emitted = 0; time_block.changed.connect (() => { changed_emitted++; }); time_block.remove_gap (gap_1); assert_null (gap_1.time_block); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); time_block.remove_gap (gap_1); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 1); time_block.remove_gap (gap_2); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); time_block.remove_gap (gap_3); assert_cmpuint (changed_emitted, GLib.CompareOperator.EQ, 2); // no change } public void test_calculate_elapsed__without_gaps () { var time_block = new Ft.TimeBlock (); time_block.set_time_range ( 5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.start_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.start_time - Ft.Interval.MINUTE)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time + Ft.Interval.MINUTE)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (10 * Ft.Interval.MINUTE)), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE) ); } public void test_calculate_elapsed__with_gaps () { var time_block = new Ft.TimeBlock (); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (5 * Ft.Interval.MINUTE, 9 * Ft.Interval.MINUTE); // 4 minutes var gap_2 = new Ft.Gap (); gap_2.set_time_range (15 * Ft.Interval.MINUTE, 17 * Ft.Interval.MINUTE); // 2 minutes var gap_3 = new Ft.Gap (); gap_3.set_time_range (29 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); // 1 minute time_block.add_gap (gap_1); time_block.add_gap (gap_2); time_block.add_gap (gap_3); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.start_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_1.end_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.start_time - Ft.Interval.MINUTE)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_3.start_time)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time + Ft.Interval.MINUTE)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_2.end_time)), new GLib.Variant.int64 ((10 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_2.start_time)), new GLib.Variant.int64 ((10 - 4) * Ft.Interval.MINUTE) ); } public void test_calculate_elapsed__with_ongoing_gap () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); var gap_1 = new Ft.Gap (); gap_1.set_time_range (now + 10 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap_1); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.start_time + Ft.Interval.MINUTE)), new GLib.Variant.int64 (Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_1.start_time)), new GLib.Variant.int64 (10 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time)), new GLib.Variant.int64 (10 * Ft.Interval.MINUTE) ); var gap_2 = new Ft.Gap (); gap_2.set_time_range (now + Ft.Interval.SECOND, now + 10 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (time_block.end_time)), new GLib.Variant.int64 (Ft.Interval.SECOND) ); } public void test_calculate_elapsed__with_gaps_overlapping () { var time_block = new Ft.TimeBlock (); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (7 * Ft.Interval.MINUTE, 11 * Ft.Interval.MINUTE); // 4 minutes var gap_2 = new Ft.Gap (); gap_2.set_time_range (10 * Ft.Interval.MINUTE, 13 * Ft.Interval.MINUTE); // 3 minutes, 1 minute ovrlapping time_block.add_gap (gap_1); time_block.add_gap (gap_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_1.end_time)), new GLib.Variant.int64 ((6 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_elapsed (gap_2.end_time)), new GLib.Variant.int64 ((8 - 4 - 3 + 1) * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__without_gaps () { var time_block = new Ft.TimeBlock (); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.start_time)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.start_time - Ft.Interval.MINUTE)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.end_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.end_time + Ft.Interval.MINUTE)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (10 * Ft.Interval.MINUTE)), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__with_gaps () { var time_block = new Ft.TimeBlock (Ft.State.STOPPED); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (2 * Ft.Interval.MINUTE, 9 * Ft.Interval.MINUTE); // 4 minutes var gap_2 = new Ft.Gap (); gap_2.set_time_range (15 * Ft.Interval.MINUTE, 17 * Ft.Interval.MINUTE); // 2 minutes var gap_3 = new Ft.Gap (); gap_3.set_time_range (29 * Ft.Interval.MINUTE, 35 * Ft.Interval.MINUTE); // 1 minute time_block.add_gap (gap_1); time_block.add_gap (gap_2); time_block.add_gap (gap_3); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.start_time)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_1.end_time)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.start_time - Ft.Interval.MINUTE)), new GLib.Variant.int64 ((25 - 1 - 2 - 4) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.end_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_3.start_time)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.end_time + Ft.Interval.MINUTE)), new GLib.Variant.int64 (0 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.end_time)), new GLib.Variant.int64 ((13 - 1) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.start_time)), new GLib.Variant.int64 ((13 - 1) * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__with_ongoing_gap () { var now = Ft.Timestamp.peek (); var time_block = new Ft.TimeBlock (Ft.State.POMODORO); time_block.set_time_range (now, now + 25 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (now + 1 * Ft.Interval.MINUTE, now + 5 * Ft.Interval.MINUTE); time_block.add_gap (gap_1); time_block.duration += gap_1.duration; var gap_2 = new Ft.Gap.with_start_time (now + 10 * Ft.Interval.MINUTE); time_block.add_gap (gap_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (time_block.start_time)), new GLib.Variant.int64 (25 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_1.start_time)), new GLib.Variant.int64 (24 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_1.end_time)), new GLib.Variant.int64 (24 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.start_time)), new GLib.Variant.int64 (19 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.start_time + Ft.Interval.HOUR)), new GLib.Variant.int64 (19 * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__with_gaps_overlapping () { var time_block = new Ft.TimeBlock (); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); var gap_1 = new Ft.Gap (); gap_1.set_time_range (7 * Ft.Interval.MINUTE, 11 * Ft.Interval.MINUTE); // 4 minutes var gap_2 = new Ft.Gap (); gap_2.set_time_range (10 * Ft.Interval.MINUTE, 13 * Ft.Interval.MINUTE); // 3 minutes, 1 minute ovrlapping time_block.add_gap (gap_1); time_block.add_gap (gap_2); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_1.start_time)), new GLib.Variant.int64 ((25 - 2 - 4 - 3 + 1) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_1.end_time)), new GLib.Variant.int64 ((25 - 6 - 3 + 1) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.start_time)), new GLib.Variant.int64 ((25 - 5 - 3) * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 (time_block.calculate_remaining (gap_2.end_time)), new GLib.Variant.int64 ((25 - 8) * Ft.Interval.MINUTE) ); } public void test_calculate_progress__without_gaps () { var time_block = new Ft.TimeBlock (); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.start_time), 0.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.start_time - Ft.Interval.MINUTE), 0.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.end_time), 1.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.end_time + Ft.Interval.MINUTE), 1.04, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (10 * Ft.Interval.MINUTE), 0.2, 0.0001); } public void test_calculate_progress__with_gaps () { var time_block = new Ft.TimeBlock (Ft.State.STOPPED); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap_1 = new Ft.Gap (); gap_1.set_time_range (2 * Ft.Interval.MINUTE, 9 * Ft.Interval.MINUTE); // 4 minutes var gap_2 = new Ft.Gap (); gap_2.set_time_range (15 * Ft.Interval.MINUTE, 17 * Ft.Interval.MINUTE); // 2 minutes var gap_3 = new Ft.Gap (); gap_3.set_time_range (29 * Ft.Interval.MINUTE, 35 * Ft.Interval.MINUTE); // 1 minute time_block.add_gap (gap_1); time_block.add_gap (gap_2); time_block.add_gap (gap_3); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.start_time), 0.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap_1.end_time), 0.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.start_time - Ft.Interval.MINUTE), 0.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.end_time), 1.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap_3.start_time), 1.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (time_block.end_time + Ft.Interval.MINUTE), 1.0, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap_2.end_time), 0.3333, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap_2.start_time), 0.3333, 0.0001); } public void test_calculate_progress__with_ongoing_gap () { var time_block = new Ft.TimeBlock (Ft.State.STOPPED); time_block.set_time_range (5 * Ft.Interval.MINUTE, 30 * Ft.Interval.MINUTE); time_block.set_completion_time (25 * Ft.Interval.MINUTE); time_block.set_status (Ft.TimeBlockStatus.IN_PROGRESS); var gap = new Ft.Gap (); gap.set_time_range (10 * Ft.Interval.MINUTE, Ft.Timestamp.UNDEFINED); time_block.add_gap (gap); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap.start_time), 0.25, 0.0001); assert_cmpfloat_with_epsilon (time_block.calculate_progress (gap.start_time + Ft.Interval.MINUTE), 0.25, 0.0001); } // public void test_changed_signal () // { // } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.TimeBlockTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-timer-view-action-group.vala000066400000000000000000000044201520625676500263400ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class TimerViewActionGroupTest : Tests.TestSuite { private Ft.Timer? timer; private Ft.SessionManager? session_manager; public TimerViewActionGroupTest () { this.add_test ("new", this.test_new); this.add_test ("start", this.test_start); // this.add_test ("stop", // this.test_stop); } public override void setup () { Ft.Timestamp.freeze_to (2000000000 * Ft.Interval.SECOND); this.timer = new Ft.Timer (); this.session_manager = new Ft.SessionManager.with_timer (this.timer); } public override void teardown () { Ft.Timestamp.thaw (); this.timer = null; this.session_manager = null; } public void test_new () { var action_group = new Ft.TimerViewActionGroup (this.session_manager); assert_true (action_group.session_manager == this.session_manager); assert_true (action_group.timer == this.timer); // TODO: check added actions } public void test_start () { var now = Ft.Timestamp.tick (0); var action_group = new Ft.TimerViewActionGroup (this.session_manager); action_group.activate_action ("start", null); assert_true (this.timer.is_running ()); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); // TODO: check current session assert_nonnull (this.session_manager.current_session); // TODO: check current time-block assert_nonnull (this.session_manager.current_time_block); } public void test_stop () { // TODO assert_not_reached (); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.TimerViewActionGroupTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-timer.vala000066400000000000000000002120511520625676500227640ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private int64 TEST_TIME = 2000000000 * Ft.Interval.SECOND; private uint get_timestamp_call_count () { var call_count = Ft.Timestamp.subtract (Ft.Timestamp.advance (0), TEST_TIME); // Vala doesn't allow casting int64 to uint, so convert through string... return uint.parse (call_count.to_string ()); } /* * Fixtures */ private Ft.TimerState create_initial_state (int64 duration = 10 * Ft.Interval.MINUTE, void* user_data = null) { return Ft.TimerState () { duration = duration, offset = 0, started_time = Ft.Timestamp.UNDEFINED, paused_time = Ft.Timestamp.UNDEFINED, finished_time = Ft.Timestamp.UNDEFINED, user_data = user_data }; } private Ft.TimerState create_started_state (int64 duration = 10 * Ft.Interval.MINUTE, int64 elapsed = 0 * Ft.Interval.MINUTE, int64 timestamp = Ft.Timestamp.UNDEFINED, void* user_data = null) { var now = Ft.Timestamp.peek (); if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = now - elapsed; } return Ft.TimerState () { duration = duration, offset = now - timestamp - elapsed, started_time = timestamp, paused_time = Ft.Timestamp.UNDEFINED, finished_time = Ft.Timestamp.UNDEFINED, user_data = user_data }; } private Ft.TimerState create_paused_state (int64 duration = 10 * Ft.Interval.MINUTE, int64 elapsed = 0 * Ft.Interval.MINUTE, int64 timestamp = Ft.Timestamp.UNDEFINED, void* user_data = null) { var now = Ft.Timestamp.peek (); if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = now - elapsed; } return Ft.TimerState () { duration = duration, offset = now - timestamp - elapsed, started_time = timestamp, paused_time = now, finished_time = Ft.Timestamp.UNDEFINED, user_data = user_data }; } private Ft.TimerState create_finished_state (int64 duration = 10 * Ft.Interval.MINUTE, int64 elapsed = 10 * Ft.Interval.MINUTE, int64 timestamp = Ft.Timestamp.UNDEFINED, void* user_data = null) { var now = Ft.Timestamp.peek (); if (Ft.Timestamp.is_undefined (timestamp)) { timestamp = now - elapsed; } return Ft.TimerState () { duration = duration, offset = now - timestamp - elapsed, started_time = timestamp, paused_time = Ft.Timestamp.UNDEFINED, finished_time = now, user_data = user_data }; } /** * Wait until timer finishes */ private bool run_timer (Ft.Timer timer, uint timeout = 0) requires (!Ft.Timestamp.is_frozen ()) { var timeout_id = (uint) 0; var cancellable = new GLib.Cancellable (); if (timeout == 0) { timeout = Ft.Timestamp.to_milliseconds_uint ( timer.calculate_remaining () + 2 * Ft.Interval.SECOND); } timeout_id = GLib.Timeout.add (timeout, () => { timeout_id = 0; cancellable.cancel (); return GLib.Source.REMOVE; }); timer.run (cancellable); if (timeout_id != 0) { GLib.Source.remove (timeout_id); } return !cancellable.is_cancelled (); } public class TimerStateTest : Tests.TestSuite { public TimerStateTest () { this.add_test ("copy", this.test_copy); } public void test_copy () { var expected_state = Ft.TimerState () { duration = 1, offset = 2, started_time = 3, paused_time = 4, finished_time = 5, user_data = GLib.MainContext.@default() }; var state = expected_state.copy (); assert_cmpvariant ( state.to_variant (), expected_state.to_variant () ); } } public class TimerTest : Tests.TestSuite { public TimerTest () { this.add_test ("new__without_args", this.test_new__without_args); this.add_test ("new__with_args", this.test_new__with_args); this.add_test ("new_with__paused_state", this.test_new_with__paused_state); this.add_test ("new_with__started_state", this.test_new_with__started_state); this.add_test ("set_default", this.test_set_default); this.add_test ("is_running", this.test_is_running); this.add_test ("is_started", this.test_is_started); this.add_test ("is_paused", this.test_is_paused); this.add_test ("is_finished", this.test_is_finished); this.add_test ("calculate_elapsed__initial_state", this.test_calculate_elapsed__initial_state); this.add_test ("calculate_elapsed__started_state", this.test_calculate_elapsed__started_state); this.add_test ("calculate_elapsed__paused_state", this.test_calculate_elapsed__paused_state); this.add_test ("calculate_elapsed__finished_state", this.test_calculate_elapsed__finished_state); this.add_test ("calculate_remaining__initial_state", this.test_calculate_remaining__initial_state); this.add_test ("calculate_remaining__started_state", this.test_calculate_remaining__started_state); this.add_test ("calculate_remaining__paused_state", this.test_calculate_remaining__paused_state); this.add_test ("calculate_remaining__finished_state", this.test_calculate_remaining__finished_state); this.add_test ("calculate_progress__initial_state", this.test_calculate_progress__initial_state); this.add_test ("calculate_progress__started_state", this.test_calculate_progress__started_state); this.add_test ("calculate_progress__paused_state", this.test_calculate_progress__paused_state); this.add_test ("calculate_progress__finished_state", this.test_calculate_progress__finished_state); this.add_test ("state", this.test_state); this.add_test ("duration", this.test_duration); this.add_test ("started_time", this.test_started_time); this.add_test ("offset", this.test_offset); this.add_test ("user_data", this.test_user_data); this.add_test ("reset", this.test_reset); this.add_test ("start__initial_state", this.test_start__initial_state); this.add_test ("start__started_state", this.test_start__started_state); this.add_test ("start__paused_state", this.test_start__paused_state); this.add_test ("start__finished_state", this.test_start__finished_state); this.add_test ("pause__initial_state", this.test_pause__initial_state); this.add_test ("pause__started_state", this.test_pause__started_state); this.add_test ("pause__paused_state", this.test_pause__paused_state); this.add_test ("pause__finished_state", this.test_pause__finished_state); this.add_test ("pause__align_to_seconds", this.test_pause__align_to_seconds); this.add_test ("resume__initial_state", this.test_resume__initial_state); this.add_test ("resume__started_state", this.test_resume__started_state); this.add_test ("resume__paused_state", this.test_resume__paused_state); this.add_test ("resume__finished_state", this.test_resume__finished_state); this.add_test ("rewind__initial_state", this.test_rewind__initial_state); this.add_test ("rewind__started_state", this.test_rewind__started_state); this.add_test ("rewind__paused_state", this.test_rewind__paused_state); this.add_test ("rewind__finished_state", this.test_rewind__finished_state); this.add_test ("rewind__align_to_seconds", this.test_rewind__align_to_seconds); this.add_test ("extend__started_state", this.test_extend__started_state); this.add_test ("extend__paused_state", this.test_extend__paused_state); this.add_test ("extend__shorten", this.test_extend__shorten); this.add_test ("extend__shorten_to_zero", this.test_extend__shorten_to_zero); this.add_test ("resolve_state_signal", this.test_resolve_state_signal); this.add_test ("state_changed_signal", this.test_state_changed_signal); this.add_test ("tick_signal", this.test_tick_signal); this.add_test ("finished_signal__0s", this.test_finished_signal__0s); this.add_test ("finished_signal__1s", this.test_finished_signal__1s); } public override void setup () { Ft.Timestamp.freeze_to (TEST_TIME); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); } public override void teardown () { Ft.Timestamp.thaw (); Ft.Timer.set_default (null); } /* * Tests for constructors */ public void test_new__without_args () { var expected_state = create_initial_state (0); var timer = new Ft.Timer (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (expected_state.duration) ); assert_false (timer.is_started ()); assert_false (timer.is_running ()); assert_false (timer.is_finished ()); // Expect constructor to not fetch system time assert_cmpvariant ( new GLib.Variant.int64 (timer.get_last_state_changed_time ()), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpuint (get_timestamp_call_count (), GLib.CompareOperator.EQ, 0); } public void test_new__with_args () { var user_data = GLib.MainContext.@default (); var expected_state = create_initial_state (Ft.Interval.MINUTE, user_data); var timer = new Ft.Timer (expected_state.duration, expected_state.user_data); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_cmpvariant ( new GLib.Variant.int64 (timer.duration), new GLib.Variant.int64 (expected_state.duration) ); assert_true (timer.user_data == expected_state.user_data); assert_false (timer.is_started ()); assert_false (timer.is_running ()); assert_false (timer.is_finished ()); } public void test_new_with__paused_state () { var paused_state = create_paused_state (); var timer = new Ft.Timer.with_state (paused_state); assert_cmpvariant ( timer.state.to_variant (), paused_state.to_variant () ); assert_true (timer.is_paused ()); assert_false (timer.is_running ()); } public void test_new_with__started_state () { var started_state = create_started_state (); var timer = new Ft.Timer.with_state (started_state); assert_cmpvariant ( timer.state.to_variant (), started_state.to_variant () ); assert_true (timer.is_started ()); assert_true (timer.is_running ()); } /* * Tests for static methods */ public void test_set_default () { // Expect timer to be created on demand var default_timer = Ft.Timer.get_default (); assert_nonnull (default_timer); // Check whether default timer holds a reference var destroyed = false; default_timer.weak_ref (() => { destroyed = true; }); default_timer = null; assert_false (destroyed); Ft.Timer.set_default (null); assert_true (destroyed); // Check setting a custom timer var custom_timer = new Ft.Timer (); assert_false (custom_timer.is_default ()); Ft.Timer.set_default (custom_timer); assert_true (Ft.Timer.get_default () == custom_timer); assert_true (custom_timer.is_default ()); } /* * Tests for properties */ public void test_state () { var timer = new Ft.Timer (); var notify_state_emitted = 0; timer.notify["state"].connect (() => { notify_state_emitted++; }); var state_1 = create_initial_state (); timer.state = state_1; assert_true (timer.state.equals (state_1)); assert_cmpint (notify_state_emitted, GLib.CompareOperator.EQ, 1); timer.state = state_1; assert_true (timer.state.equals (state_1)); assert_cmpint (notify_state_emitted, GLib.CompareOperator.EQ, 1); // unchanged var state_2 = create_started_state (); timer.state = state_2; assert_true (timer.state.equals (state_2)); assert_cmpint (notify_state_emitted, GLib.CompareOperator.EQ, 2); } public void test_duration () { var timer = new Ft.Timer (); var notify_duration_emitted = 0; timer.notify["duration"].connect (() => { notify_duration_emitted++; }); var duration_1 = Ft.Interval.MINUTE; timer.duration = duration_1; assert_true (timer.duration == duration_1); assert_cmpint (notify_duration_emitted, GLib.CompareOperator.EQ, 1); timer.duration = duration_1; assert_true (timer.duration == duration_1); assert_cmpint (notify_duration_emitted, GLib.CompareOperator.EQ, 1); // unchanged var duration_2 = 2 * Ft.Interval.MINUTE; timer.duration = duration_2; assert_true (timer.duration == duration_2); assert_cmpint (notify_duration_emitted, GLib.CompareOperator.EQ, 2); } public void test_started_time () { var timer = new Ft.Timer (); var notify_started_time_emitted = 0; timer.notify["started-time"].connect (() => { notify_started_time_emitted++; }); var state_1 = create_started_state (); timer.state = state_1; assert_cmpint (notify_started_time_emitted, GLib.CompareOperator.EQ, 1); var state_2 = state_1.copy(); state_2.user_data = new GLib.Object (); timer.state = state_2; assert_cmpint (notify_started_time_emitted, GLib.CompareOperator.EQ, 1); // unchanged Ft.Timestamp.advance (Ft.Interval.MINUTE); var state_3 = create_started_state (); timer.state = state_3; assert_cmpint (notify_started_time_emitted, GLib.CompareOperator.EQ, 2); } public void test_offset () { var timer = new Ft.Timer (); var notify_offset_emitted = 0; timer.notify["offset"].connect (() => { notify_offset_emitted++; }); var state_1 = create_started_state (); state_1.offset = Ft.Interval.MINUTE; timer.state = state_1; assert_cmpint (notify_offset_emitted, GLib.CompareOperator.EQ, 1); var state_2 = create_started_state (); state_2.offset = Ft.Interval.MINUTE; timer.state = state_2; assert_cmpint (notify_offset_emitted, GLib.CompareOperator.EQ, 1); // unchanged var state_3 = create_started_state (); state_3.offset = 2 * Ft.Interval.MINUTE; timer.state = state_3; assert_cmpint (notify_offset_emitted, GLib.CompareOperator.EQ, 2); } public void test_user_data () { var timer = new Ft.Timer (); var notify_user_data_emitted = 0; timer.notify["user-data"].connect (() => { notify_user_data_emitted++; }); var user_data_1 = new GLib.Object (); timer.user_data = user_data_1; assert_true (timer.user_data == user_data_1); assert_cmpint (notify_user_data_emitted, GLib.CompareOperator.EQ, 1); timer.user_data = user_data_1; assert_true (timer.user_data == user_data_1); assert_cmpint (notify_user_data_emitted, GLib.CompareOperator.EQ, 1); // unchanged var user_data_2 = new GLib.Object (); timer.user_data = user_data_2; assert_true (timer.user_data == user_data_2); assert_cmpint (notify_user_data_emitted, GLib.CompareOperator.EQ, 2); } /* * Tests for .is_*() methods */ public void test_is_running () { assert_true ( new Ft.Timer.with_state (create_started_state ()).is_running () ); assert_false ( new Ft.Timer.with_state (create_initial_state ()).is_running () ); assert_false ( new Ft.Timer.with_state (create_paused_state ()).is_running () ); assert_false ( new Ft.Timer.with_state (create_finished_state ()).is_running () ); } public void test_is_started () { assert_true ( new Ft.Timer.with_state (create_started_state ()).is_started () ); assert_true ( new Ft.Timer.with_state (create_paused_state ()).is_started () ); assert_true ( new Ft.Timer.with_state (create_finished_state ()).is_started () ); assert_false ( new Ft.Timer.with_state (create_initial_state ()).is_started () ); } public void test_is_paused () { assert_true ( new Ft.Timer.with_state (create_paused_state ()).is_paused () ); assert_false ( new Ft.Timer.with_state (create_initial_state ()).is_paused () ); assert_false ( new Ft.Timer.with_state (create_started_state ()).is_paused () ); assert_false ( new Ft.Timer.with_state (create_finished_state ()).is_paused () ); } public void test_is_finished () { assert_true ( new Ft.Timer.with_state (create_finished_state ()).is_finished () ); assert_false ( new Ft.Timer.with_state (create_initial_state ()).is_finished () ); assert_false ( new Ft.Timer.with_state (create_started_state ()).is_finished () ); assert_false ( new Ft.Timer.with_state (create_paused_state ()).is_finished () ); } /* * Tests for .calculate_elapsed() */ public void test_calculate_elapsed__initial_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_initial_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (0) ); } public void test_calculate_elapsed__started_state () { var timer = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (timer.state.started_time - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (timer.state.started_time + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (timer.state.started_time + timer.duration + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (timer.duration) ); var now = Ft.Timestamp.peek (); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var timer_with_offset = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer_with_offset.calculate_elapsed ( timer_with_offset.state.started_time + timer_with_offset.state.offset + Ft.Interval.MINUTE ) ), new GLib.Variant.int64 (Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer_with_offset.calculate_elapsed ( timer_with_offset.state.started_time + 5 * Ft.Interval.MINUTE ) ), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); } public void test_calculate_elapsed__paused_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_paused_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE) // estimation ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now) ), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); } public void test_calculate_elapsed__finished_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_finished_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (3 * Ft.Interval.MINUTE) // estimation ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now) ), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_elapsed (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); } /* * Tests for .calculate_remaining() */ public void test_calculate_remaining__initial_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_initial_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__started_state () { var timer = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (timer.state.started_time - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (20 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (timer.state.started_time + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (19 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (timer.state.started_time + timer.duration + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (0) ); var now = Ft.Timestamp.peek (); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var timer_with_offset = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer_with_offset.calculate_remaining ( timer_with_offset.state.started_time + timer_with_offset.state.offset + Ft.Interval.MINUTE ) ), new GLib.Variant.int64 (19 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer_with_offset.calculate_remaining ( timer_with_offset.state.started_time + 5 * Ft.Interval.MINUTE ) ), new GLib.Variant.int64 (16 * Ft.Interval.MINUTE) ); } public void test_calculate_remaining__paused_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_paused_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (17 * Ft.Interval.MINUTE) // estimation ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now) ), new GLib.Variant.int64 (16 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (16 * Ft.Interval.MINUTE) ); } /** * Timer duration should have precedence over marking timer as finished. * Therefore, finished timer can still have some remaining time. */ public void test_calculate_remaining__finished_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_finished_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now - Ft.Interval.MINUTE) ), new GLib.Variant.int64 (17 * Ft.Interval.MINUTE) // estimation ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now) ), new GLib.Variant.int64 (16 * Ft.Interval.MINUTE) ); assert_cmpvariant ( new GLib.Variant.int64 ( timer.calculate_remaining (now + Ft.Interval.MINUTE) ), new GLib.Variant.int64 (16 * Ft.Interval.MINUTE) ); } /* * Tests for .calculate_progress() */ public void test_calculate_progress__initial_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_initial_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpvariant ( timer.calculate_progress (now - Ft.Interval.MINUTE), 0.0 ); assert_cmpvariant ( timer.calculate_progress (now + Ft.Interval.MINUTE), 0.0 ); } public void test_calculate_progress__started_state () { var timer = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE ) ); assert_cmpfloat ( timer.calculate_progress (timer.state.started_time - Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 0.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (timer.state.started_time + Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 1.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (timer.state.started_time + timer.duration + Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 20.0 / 20.0 ); var now = Ft.Timestamp.peek (); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); var timer_with_offset = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpfloat ( timer_with_offset.calculate_progress ( timer_with_offset.state.started_time + timer_with_offset.state.offset + Ft.Interval.MINUTE ), GLib.CompareOperator.EQ, 1.0 / 20.0 ); assert_cmpfloat ( timer_with_offset.calculate_progress ( timer_with_offset.state.started_time + 5 * Ft.Interval.MINUTE ), GLib.CompareOperator.EQ, 4.0 / 20.0 ); } public void test_calculate_progress__paused_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_paused_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpfloat ( timer.calculate_progress (now - Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 3.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (now), GLib.CompareOperator.EQ, 4.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (now + Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 4.0 / 20.0 ); } /** * Timer duration should have precedence over marking timer as finished. * Therefore, finished timer can still have some remaining time. */ public void test_calculate_progress__finished_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_finished_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE, now - 5 * Ft.Interval.MINUTE ) ); assert_cmpfloat ( timer.calculate_progress (now - Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 3.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (now), GLib.CompareOperator.EQ, 4.0 / 20.0 ); assert_cmpfloat ( timer.calculate_progress (now + Ft.Interval.MINUTE), GLib.CompareOperator.EQ, 4.0 / 20.0 ); } /* * Tests for .reset() */ public void test_reset () { var expected_state = Ft.TimerState (); expected_state.duration = Ft.Interval.SECOND; var signals = new string[0]; var timer = new Ft.Timer.with_state ( Ft.TimerState () { duration = Ft.Interval.MINUTE, offset = 1, started_time = 2, paused_time = 3, finished_time = 4 } ); timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; }); timer.finished.connect (() => { signals += "finished"; }); timer.reset (expected_state.duration); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_started ()); assert_false (timer.is_running ()); assert_false (timer.is_finished ()); assert_cmpstrv (signals, {"resolve-state", "state-changed"}); } /* * Tests for .start() */ public void test_start__initial_state () { var now = Ft.Timestamp.peek (); var signals = new string[0]; var state_changed_time = Ft.Timestamp.UNDEFINED; var initial_state = create_initial_state (); var expected_state = initial_state.copy (); expected_state.started_time = now + 5 * Ft.Interval.MINUTE; expected_state.paused_time = Ft.Timestamp.UNDEFINED; var timer = new Ft.Timer.with_state (initial_state); timer.resolve_state.connect (() => { signals += "resolve-state"; }); timer.state_changed.connect (() => { signals += "state-changed"; state_changed_time = timer.get_last_state_changed_time (); }); now = Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.start (now); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_started ()); assert_true (timer.is_running ()); assert_cmpstrv (signals, {"resolve-state", "state-changed"}); assert_cmpvariant ( new GLib.Variant.int64 (state_changed_time), new GLib.Variant.int64 (timer.state.started_time) ); } /** * Starting from already started state. * * Expect call to be ignored. */ public void test_start__started_state () { var started_state = create_started_state (); var expected_state = started_state.copy (); var timer = new Ft.Timer.with_state (started_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.start (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_started ()); assert_true (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Starting from paused state. * * Expect call to be ignored. If you want to resume timer you should use `.resume()`. */ public void test_start__paused_state () { var paused_state = create_paused_state ( 20 * Ft.Interval.MINUTE, 4 * Ft.Interval.MINUTE ); var expected_state = paused_state.copy (); var timer = new Ft.Timer.with_state (paused_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.start (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Starting from finished state. * * Expect call to be ignored. */ public void test_start__finished_state () { var finished_state = create_finished_state (); var expected_state = finished_state.copy (); var timer = new Ft.Timer.with_state (finished_state); Ft.Timestamp.advance (1 * Ft.Interval.MINUTE); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); timer.start (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /* * Tests for .pause() */ /** * Pausing from initial state. Expect call to be ignored. */ public void test_pause__initial_state () { var initial_state = create_initial_state (); var expected_state = initial_state.copy (); var timer = new Ft.Timer.with_state (initial_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.pause (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Pausing a started should preserve elapsed time. */ public void test_pause__started_state () { var started_state = create_started_state (); var expected_state = started_state.copy (); expected_state.paused_time = expected_state.started_time + 5 * Ft.Interval.MINUTE; var timer = new Ft.Timer.with_state (started_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.freeze_to (expected_state.paused_time); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); timer.pause (expected_state.paused_time); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (expected_state.paused_time)), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE) ); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_started ()); assert_true (timer.is_paused ()); assert_false (timer.is_running ()); assert_false (timer.is_finished ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Pausing a paused state should ignore the call. */ public void test_pause__paused_state () { var paused_state = create_paused_state (); var expected_state = paused_state.copy (); var timer = new Ft.Timer.with_state (paused_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (1 * Ft.Interval.MINUTE); timer.pause (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_started ()); assert_true (timer.is_paused ()); assert_false (timer.is_running ()); assert_false (timer.is_finished ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Pausing from finished state. Expect call to be ignored. */ public void test_pause__finished_state () { var finished_state = create_finished_state (); var expected_state = finished_state.copy (); var timer = new Ft.Timer.with_state (finished_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (1 * Ft.Interval.MINUTE); timer.pause (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * After resumimg timer we want to wait roughly 1s for the next tick. * This implies that elapsed time should be rounded. */ public void test_pause__align_to_seconds () { var started_state = create_started_state (); var timer = new Ft.Timer.with_state (started_state); var pause_time = timer.state.started_time + 3200 * Ft.Interval.MILLISECOND; Ft.Timestamp.freeze_to (pause_time); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (pause_time)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); timer.pause (pause_time); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (pause_time)), new GLib.Variant.int64 (3 * Ft.Interval.SECOND) ); } /* * Tests for .resume() */ /** * Resuming from initial state. Expect call to be ignored. */ public void test_resume__initial_state () { var initial_state = create_initial_state (); var expected_state = initial_state.copy (); var timer = new Ft.Timer.with_state (initial_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.resume (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Resuming from started state. Expect call to be ignored. */ public void test_resume__started_state () { var started_state = create_started_state (); var expected_state = started_state.copy (); var timer = new Ft.Timer.with_state (started_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.resume (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Resuming a paused state. */ public void test_resume__paused_state () { var now = Ft.Timestamp.peek (); var paused_state = create_paused_state (); var expected_state = paused_state.copy (); expected_state.offset += 1 * Ft.Interval.MINUTE; expected_state.paused_time = Ft.Timestamp.UNDEFINED; var timer = new Ft.Timer.with_state (paused_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.freeze_to (now + Ft.Interval.MINUTE); Ft.Timestamp.set_auto_advance (Ft.Interval.MICROSECOND); timer.resume (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_true (timer.is_running ()); assert_true (timer.is_started ()); assert_false (timer.is_paused ()); assert_false (timer.is_finished ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); } /** * Resuming from finished state. Expect call to be ignored. */ public void test_resume__finished_state () { var finished_state = create_finished_state (); var expected_state = finished_state.copy (); var timer = new Ft.Timer.with_state (finished_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (1 * Ft.Interval.MINUTE); timer.resume (); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /* * Tests for .rewind() */ /** * Rewinding an initial state. Expect call to be ignored. */ public void test_rewind__initial_state () { var initial_state = create_initial_state (); var expected_state = initial_state.copy (); var timer = new Ft.Timer.with_state (initial_state); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); Ft.Timestamp.advance (5 * Ft.Interval.MINUTE); timer.rewind (Ft.Interval.MINUTE); assert_cmpvariant ( timer.state.to_variant (), expected_state.to_variant () ); assert_false (timer.is_running ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 0); } /** * Rewinding the timer expect to only alter the offset, * not the started_time. */ public void test_rewind__started_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_started_state ( 20 * Ft.Interval.MINUTE, 5 * Ft.Interval.MINUTE, now - 7 * Ft.Interval.MINUTE ) ); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); var expected_started_time = timer.state.started_time; // Rewind 1 minute timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_true (timer.is_running ()); assert_false (timer.is_paused ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); // Rewind 5 minutes timer.rewind (5 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_true (timer.is_running ()); assert_false (timer.is_paused ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); } /** * Rewind a paused timer. * * There is no one obvious way to perform a `rewind` here. * Our take is to resume the timer and only alter `state.offset`. */ public void test_rewind__paused_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_paused_state ( 20 * Ft.Interval.MINUTE, 5 * Ft.Interval.MINUTE, now - 7 * Ft.Interval.MINUTE ) ); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); var expected_started_time = timer.state.started_time; assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed ()), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE) ); // Rewind 1 minute timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_false (timer.is_running ()); assert_true (timer.is_paused ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); // Rewind 5 minutes timer.rewind (5 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_false (timer.is_running ()); assert_true (timer.is_paused ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); } public void test_rewind__finished_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_finished_state ( 20 * Ft.Interval.MINUTE, 5 * Ft.Interval.MINUTE, now - 7 * Ft.Interval.MINUTE ) ); var state_changed_emitted = 0; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; }); var expected_started_time = timer.state.started_time; // Rewind 1 minute timer.rewind (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE) ); assert_true (timer.is_running ()); assert_false (timer.is_paused ()); assert_false (timer.is_finished ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); // Rewind 5 minutes timer.rewind (5 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.state.started_time), new GLib.Variant.int64 (expected_started_time) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (0) ); assert_true (timer.is_running ()); assert_false (timer.is_paused ()); assert_false (timer.is_finished ()); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); } /** * After rewinding timer we want to wait roughly 1s for the next tick. * This implies that elapsed time should be rounded. */ public void test_rewind__align_to_seconds () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_started_state ( Ft.Interval.MINUTE, 3200 * Ft.Interval.MILLISECOND ) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); // Rewind 1s. Expect tick time and elapsed time to be rounded to seconds. timer.rewind (Ft.Interval.SECOND, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (2000 * Ft.Interval.MILLISECOND) ); // Rewind to start. Expect displayed time will be same as state duration. timer.state_changed.connect (() => { var timestamp = timer.get_last_state_changed_time (); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (timestamp)), new GLib.Variant.int64 (0) ); }); now = Ft.Timestamp.advance (100 * Ft.Interval.MILLISECOND); timer.rewind (Ft.Interval.MINUTE, now); } /* * Tests for .extend() */ public void test_extend__started_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_started_state ( 5 * Ft.Interval.MINUTE, 3200 * Ft.Interval.MILLISECOND ) ); timer.extend (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE - 3200 * Ft.Interval.MILLISECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); } public void test_extend__paused_state () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_paused_state ( 5 * Ft.Interval.MINUTE, 3200 * Ft.Interval.MILLISECOND, now ) ); timer.extend (Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (6 * Ft.Interval.MINUTE - 3200 * Ft.Interval.MILLISECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); } public void test_extend__shorten () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_started_state ( 5 * Ft.Interval.MINUTE, 3200 * Ft.Interval.MILLISECOND, now ) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (5 * Ft.Interval.MINUTE - 3200 * Ft.Interval.MILLISECOND) ); timer.extend (-Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (4 * Ft.Interval.MINUTE - 3200 * Ft.Interval.MILLISECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); } public void test_extend__shorten_to_zero () { var now = Ft.Timestamp.peek (); var timer = new Ft.Timer.with_state ( create_started_state ( 5 * Ft.Interval.MINUTE, 3200 * Ft.Interval.MILLISECOND, now ) ); timer.extend (-10 * Ft.Interval.MINUTE, now); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_remaining (now)), new GLib.Variant.int64 (10 * Ft.Interval.SECOND) ); assert_cmpvariant ( new GLib.Variant.int64 (timer.calculate_elapsed (now)), new GLib.Variant.int64 (3200 * Ft.Interval.MILLISECOND) ); } /* * Tests for signals */ public void test_state_changed_signal () { var timer = new Ft.Timer (1 * Ft.Interval.MINUTE); var state_changed_emitted = 0; var state_changed_time = Ft.Timestamp.UNDEFINED; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; state_changed_time = timer.get_last_state_changed_time (); }); timer.start (); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 1); assert_cmpvariant ( new GLib.Variant.int64 (timer.get_last_state_changed_time ()), new GLib.Variant.int64 (timer.state.started_time) ); } /** * Check behaviour of changing the state during resolve_state emission. * * Expect new resolve-state signal to be emitted and one state-changed signal at the end. */ public void test_resolve_state_signal () { var paused_state = create_paused_state (); var resolve_state_emitted = 0; var timer = new Ft.Timer.with_state (create_initial_state ()); timer.resolve_state.connect ((ref state) => { resolve_state_emitted++; if (resolve_state_emitted >= 100) { return; } if (state.started_time >= 0 && state.paused_time < 0) { timer.state = paused_state; } }); // Expect started state to be resolved into paused state timer.start (); // Ensure there is no infinite recursion assert_cmpint (resolve_state_emitted, GLib.CompareOperator.LT, 100); assert_cmpvariant ( timer.state.to_variant (), paused_state.to_variant () ); } public void test_finished_signal__0s () { Ft.Timestamp.thaw (); var timer = new Ft.Timer (0 * Ft.Interval.SECOND); var finished_emitted = 0; var state_changed_emitted = 0; var finished_time_in_state_changed = Ft.Timestamp.UNDEFINED; var finished_time_in_finished = Ft.Timestamp.UNDEFINED; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; finished_time_in_state_changed = current_state.finished_time; }); // Expect finished signal to be emitted AFTER state_changed // and the state should already have finished_time set timer.finished.connect ((state) => { finished_emitted++; finished_time_in_finished = state.finished_time; assert_cmpint (state_changed_emitted, GLib.CompareOperator.GT, 0); assert_true (Ft.Timestamp.is_defined (finished_time_in_state_changed)); assert_cmpvariant ( new GLib.Variant.int64 (finished_time_in_finished), new GLib.Variant.int64 (finished_time_in_state_changed) ); }); timer.start (); assert_false (timer.is_running ()); assert_true (timer.is_finished ()); assert_true (run_timer (timer)); assert_cmpint (finished_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); timer.start (); assert_false (timer.is_running ()); assert_true (timer.is_finished ()); assert_cmpint (finished_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); } public void test_finished_signal__1s () { Ft.Timestamp.thaw (); var timer = new Ft.Timer (1 * Ft.Interval.SECOND); var finished_emitted = 0; var state_changed_emitted = 0; var finished_time_in_state_changed = Ft.Timestamp.UNDEFINED; var finished_time_in_finished = Ft.Timestamp.UNDEFINED; timer.state_changed.connect ((current_state, previous_state) => { state_changed_emitted++; finished_time_in_state_changed = current_state.finished_time; }); // Expect finished signal to be emitted AFTER state_changed // and the state should already have finished_time set timer.finished.connect ((state) => { finished_emitted++; finished_time_in_finished = state.finished_time; assert_cmpint (state_changed_emitted, GLib.CompareOperator.GT, 0); assert_true (Ft.Timestamp.is_defined (finished_time_in_state_changed)); assert_cmpvariant ( new GLib.Variant.int64 (finished_time_in_finished), new GLib.Variant.int64 (finished_time_in_state_changed) ); }); timer.start (); assert_true (timer.is_running ()); assert_false (timer.is_finished ()); assert_true (run_timer (timer)); assert_cmpint (finished_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); timer.start (); assert_false (timer.is_running ()); assert_true (timer.is_finished ()); assert_cmpint (finished_emitted, GLib.CompareOperator.EQ, 1); assert_cmpint (state_changed_emitted, GLib.CompareOperator.EQ, 2); } public void test_tick_signal () { Ft.Timestamp.thaw (); var timer = new Ft.Timer (3 * Ft.Interval.SECOND); var call_count = 0; int64 real_times[5] = {-1, -1, -1, -1 -1}; int64 tick_times[5] = {-1, -1, -1, -1 -1}; int64 sum_deviation = 0; timer.tick.connect ((_timestamp) => { if (call_count < 5) { real_times[call_count] = timer.calculate_elapsed (timer.get_current_time ()); tick_times[call_count] = timer.calculate_elapsed (_timestamp); sum_deviation += real_times[call_count] - tick_times[call_count]; } call_count++; }); timer.start (); assert_true (run_timer (timer)); assert_cmpint (call_count, GLib.CompareOperator.GE, 2); var mean_deviation = sum_deviation / call_count; assert_cmpuint (Ft.Timestamp.to_milliseconds_uint (mean_deviation.abs ()), GLib.CompareOperator.LT, 100); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.TimerStateTest (), new Tests.TimerTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-timestamp.vala000066400000000000000000000260411520625676500236510ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class TimestampTest : Tests.TestSuite { public TimestampTest () { this.add_test ("round", this.test_round); this.add_test ("round__undefined", this.test_round__undefined); this.add_test ("add_interval", this.test_add_interval); this.add_test ("subtract", this.test_subtract); this.add_test ("subtract_interval", this.test_subtract_interval); this.add_test ("to_iso8601", this.test_to_iso8601); } public override void setup () { } public override void teardown () { } public void test_round () { var unit = Ft.Interval.SECOND; var timestamp_lower = 20 * unit; var timestamp_upper = timestamp_lower + unit; var timestamp_1 = timestamp_lower + 1; var timestamp_2 = timestamp_lower + (unit / 2 - 1); var timestamp_3 = timestamp_lower + (unit / 2); var timestamp_4 = timestamp_lower + (unit / 2 + 1); var timestamp_5 = timestamp_lower + (unit - 1); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (timestamp_1, unit)), new GLib.Variant.int64 (timestamp_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (timestamp_2, unit)), new GLib.Variant.int64 (timestamp_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (timestamp_3, unit)), new GLib.Variant.int64 (timestamp_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (timestamp_4, unit)), new GLib.Variant.int64 (timestamp_upper) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (timestamp_5, unit)), new GLib.Variant.int64 (timestamp_upper) ); } public void test_round__undefined () { var unit = Ft.Interval.SECOND; assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.round (Ft.Timestamp.UNDEFINED, unit)), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); } public void test_add_interval () { var interval = Ft.Interval.MINUTE; var timestamp_1 = Ft.Timestamp.from_now (); var timestamp_2 = timestamp_1 + interval; assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (timestamp_1, 0)), new GLib.Variant.int64 (timestamp_1) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (timestamp_1, interval)), new GLib.Variant.int64 (timestamp_2) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (Ft.Timestamp.UNDEFINED, interval)), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (Ft.Timestamp.MIN, interval)), new GLib.Variant.int64 (interval) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (Ft.Timestamp.MAX, interval)), new GLib.Variant.int64 (Ft.Timestamp.MAX) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.add_interval (Ft.Timestamp.MIN, -interval)), new GLib.Variant.int64 (Ft.Timestamp.MIN) ); } public void test_subtract () { var interval = Ft.Interval.MINUTE; var timestamp_1 = Ft.Timestamp.from_now (); var timestamp_2 = timestamp_1 + interval; assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract (timestamp_1, 0)), new GLib.Variant.int64 (timestamp_1) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract (timestamp_2, timestamp_1)), new GLib.Variant.int64 (interval) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract (timestamp_1, timestamp_2)), new GLib.Variant.int64 (-interval) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract (timestamp_1, Ft.Timestamp.UNDEFINED)), new GLib.Variant.int64 (0) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract (Ft.Timestamp.UNDEFINED, timestamp_1)), new GLib.Variant.int64 (0) ); } public void test_subtract_interval () { var interval = Ft.Interval.MINUTE; var timestamp_1 = Ft.Timestamp.from_now (); var timestamp_2 = timestamp_1 + interval; assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract_interval (timestamp_1, 0)), new GLib.Variant.int64 (timestamp_1) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract_interval (timestamp_2, interval)), new GLib.Variant.int64 (timestamp_1) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract_interval (Ft.Timestamp.UNDEFINED, interval)), new GLib.Variant.int64 (Ft.Timestamp.UNDEFINED) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract_interval (Ft.Timestamp.MIN, interval)), new GLib.Variant.int64 (Ft.Timestamp.MIN) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Timestamp.subtract_interval (Ft.Timestamp.MAX, -interval)), new GLib.Variant.int64 (Ft.Timestamp.MAX) ); } public void test_to_iso8601 () { var timestamp_1 = (int64) 0; var timestamp_2 = timestamp_1 + Ft.Interval.MICROSECOND; var timestamp_3 = timestamp_1 + Ft.Interval.MILLISECOND; var timestamp_4 = timestamp_1 + Ft.Interval.SECOND - Ft.Interval.MICROSECOND; var timestamp_5 = Ft.Timestamp.from_seconds_uint (1014304205); assert_cmpstr ( Ft.Timestamp.to_iso8601 (timestamp_1), GLib.CompareOperator.EQ, "1970-01-01T00:00:00Z" ); assert_cmpstr ( Ft.Timestamp.to_iso8601 (timestamp_2), GLib.CompareOperator.EQ, "1970-01-01T00:00:00.000001Z" ); assert_cmpstr ( Ft.Timestamp.to_iso8601 (timestamp_3), GLib.CompareOperator.EQ, "1970-01-01T00:00:00.001000Z" ); assert_cmpstr ( Ft.Timestamp.to_iso8601 (timestamp_4), GLib.CompareOperator.EQ, "1970-01-01T00:00:00.999999Z" ); assert_cmpstr ( Ft.Timestamp.to_iso8601 (timestamp_5), GLib.CompareOperator.EQ, "2002-02-21T15:10:05Z" ); } } public class IntervalTest : Tests.TestSuite { public IntervalTest () { this.add_test ("round__positive", this.test_round__positive); this.add_test ("round__negative", this.test_round__negative); } public override void setup () { } public override void teardown () { } public void test_round__positive () { var unit = Ft.Interval.SECOND; var interval_lower = 20 * unit; var interval_upper = interval_lower + unit; var interval_1 = interval_lower + 1; var interval_2 = interval_lower + (unit / 2 - 1); var interval_3 = interval_lower + (unit / 2); var interval_4 = interval_lower + (unit / 2 + 1); var interval_5 = interval_lower + (unit - 1); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_1, unit)), new GLib.Variant.int64 (interval_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_2, unit)), new GLib.Variant.int64 (interval_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_3, unit)), new GLib.Variant.int64 (interval_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_4, unit)), new GLib.Variant.int64 (interval_upper) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_5, unit)), new GLib.Variant.int64 (interval_upper) ); } public void test_round__negative () { var unit = Ft.Interval.SECOND; var interval_upper = -20 * unit; var interval_lower = interval_upper - unit; var interval_1 = interval_upper - 1; var interval_2 = interval_upper - (unit / 2 - 1); var interval_3 = interval_upper - (unit / 2); var interval_4 = interval_upper - (unit / 2 + 1); var interval_5 = interval_upper - (unit - 1); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_1, unit)), new GLib.Variant.int64 (interval_upper) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_2, unit)), new GLib.Variant.int64 (interval_upper) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_3, unit)), new GLib.Variant.int64 (interval_upper) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_4, unit)), new GLib.Variant.int64 (interval_lower) ); assert_cmpvariant ( new GLib.Variant.int64 (Ft.Interval.round (interval_5, unit)), new GLib.Variant.int64 (interval_lower) ); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.TimestampTest (), new Tests.IntervalTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-timezone-history.vala000066400000000000000000000277441520625676500252120ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { private int64[] generate_random_timestamps (uint size) { var timestamps = new int64[size]; var timestamp = (int64) 0; for (var index = 0; index < size; index++) { timestamp += GLib.Random.int_range (1, 1000); timestamps[index] = timestamp; } return timestamps; } private GLib.Variant int64_array_to_variant (int64[] values) { var children = new GLib.Variant[values.length]; for (var index = 0; index < values.length; index++) { children[index] = new GLib.Variant.int64 (values[index]); } return new GLib.Variant.array (GLib.VariantType.INT64, children); } public class TimezoneHistoryTest : Tests.TestSuite { private GLib.TimeZone? new_york_timezone; private GLib.TimeZone? london_timezone; private GLib.TimeZone? tokyo_timezone; public TimezoneHistoryTest () { this.add_test ("insert__reverse_order", this.test_insert__reverse_order); this.add_test ("insert__replace", this.test_insert__replace); this.add_test ("insert__duplicate", this.test_insert__duplicate); this.add_test ("search__null", this.test_search__null); this.add_test ("search__exact", this.test_search__exact); this.add_test ("search__closest", this.test_search__closest); this.add_test ("scan__timezones", this.test_scan__timezones); this.add_test ("scan__dst_switch", this.test_scan__dst_switch); this.add_test ("fetch", this.test_fetch); this.add_test ("fetch__max", this.test_fetch__max); } public override void setup () { try { this.new_york_timezone = new GLib.TimeZone.identifier ("America/New_York"); this.london_timezone = new GLib.TimeZone.identifier ("Europe/London"); this.tokyo_timezone = new GLib.TimeZone.identifier ("Asia/Tokyo"); } catch (GLib.Error error) { assert_no_error (error); } Ft.Database.open (); } public override void teardown () { Ft.Database.close (); } public void test_insert__reverse_order () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (400, this.new_york_timezone); timezone_history.insert (300, this.london_timezone); timezone_history.insert (200, this.new_york_timezone); unowned var timezone_1 = timezone_history.search (200); assert_nonnull (timezone_1); assert_cmpstr (timezone_1.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); unowned var timezone_2 = timezone_history.search (300); assert_nonnull (timezone_2); assert_cmpstr (timezone_2.get_identifier (), GLib.CompareOperator.EQ, "Europe/London"); unowned var timezone_3 = timezone_history.search (400); assert_nonnull (timezone_3); assert_cmpstr (timezone_3.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); } public void test_insert__replace () { GLib.TimeZone? timezone; var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); timezone = timezone_history.search (200); assert_nonnull (timezone); assert_cmpstr (timezone.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); timezone_history.insert (200, this.london_timezone); timezone = timezone_history.search (200); assert_nonnull (timezone); assert_cmpstr (timezone.get_identifier (), GLib.CompareOperator.EQ, "Europe/London"); } public void test_insert__duplicate () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); timezone_history.insert (300, this.new_york_timezone); unowned var timezone = timezone_history.search (300); assert_nonnull (timezone); assert_cmpstr (timezone.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); } public void test_search__null () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); unowned var timezone = timezone_history.search (199); assert_null (timezone); } public void test_search__exact () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); timezone_history.insert (300, this.london_timezone); timezone_history.insert (400, this.new_york_timezone); unowned var timezone_1 = timezone_history.search (200); assert_nonnull (timezone_1); assert_cmpstr (timezone_1.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); unowned var timezone_2 = timezone_history.search (300); assert_nonnull (timezone_2); assert_cmpstr (timezone_2.get_identifier (), GLib.CompareOperator.EQ, "Europe/London"); unowned var timezone_3 = timezone_history.search (400); assert_nonnull (timezone_3); assert_cmpstr (timezone_3.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); } public void test_search__closest () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); timezone_history.insert (300, this.london_timezone); unowned var timezone_1 = timezone_history.search (201); assert_nonnull (timezone_1); assert_cmpstr (timezone_1.get_identifier (), GLib.CompareOperator.EQ, "America/New_York"); unowned var timezone_2 = timezone_history.search (400); assert_nonnull (timezone_2); assert_cmpstr (timezone_2.get_identifier (), GLib.CompareOperator.EQ, "Europe/London"); } private void test_scan (Ft.TimezoneHistory timezone_history, int64 start_time, int64 end_time, int64[] expected_start_times, int64[] expected_end_times, string[] expected_timezone_identifiers) { int64[] start_times = {}; int64[] end_times = {}; string[] timezone_identifiers = {}; timezone_history.scan ( start_time, end_time, (_start_time, _end_time, timezone) => { start_times += _start_time; end_times += _end_time; timezone_identifiers += timezone.get_identifier (); }); assert_cmpstrv ( timezone_identifiers, expected_timezone_identifiers); assert_cmpvariant ( int64_array_to_variant (start_times), int64_array_to_variant (expected_start_times)); assert_cmpvariant ( int64_array_to_variant (end_times), int64_array_to_variant (expected_end_times)); } public void test_scan__timezones () { var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (200, this.new_york_timezone); timezone_history.insert (300, this.london_timezone); timezone_history.insert (400, this.tokyo_timezone); this.test_scan ( timezone_history, 250, 450, {250, 300, 400}, {300, 400, 450}, {"America/New_York", "Europe/London", "Asia/Tokyo"}); this.test_scan ( timezone_history, 100, 199, {}, {}, {}); this.test_scan ( timezone_history, 450, 500, {450}, {500}, {"Asia/Tokyo"}); } public void test_scan__dst_switch () { var dst_switch_time = Ft.Timestamp.from_datetime ( new GLib.DateTime (this.new_york_timezone, 2000, 4, 2, 2, 0, 0)); var start_time = dst_switch_time - Ft.Interval.MINUTE; var end_time = dst_switch_time + Ft.Interval.MINUTE; var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (start_time, this.new_york_timezone); this.test_scan ( timezone_history, start_time, end_time, {start_time, dst_switch_time}, {dst_switch_time, end_time}, {"America/New_York", "America/New_York"}); } public void test_fetch () { var timezone_history = new Ft.TimezoneHistory (); var timestamps = generate_random_timestamps (1000); for (var index = 0; index < timestamps.length; index++) { timezone_history.insert ( timestamps[index], (index & 1) == 0 ? this.new_york_timezone : this.london_timezone); } // Destroy one instance to check if entries have been saved and a new instance is // fetching it properly. timezone_history = null; timezone_history = new Ft.TimezoneHistory (); for (var index = 0; index < timestamps.length; index++) { unowned var timezone = timezone_history.search (timestamps[index]); assert_nonnull (timezone); assert_cmpstr ( timezone.get_identifier (), GLib.CompareOperator.EQ, (index & 1) == 0 ? "America/New_York" : "Europe/London" ); } } public void test_fetch__max () { var timestamp = Ft.Timestamp.MAX; var timezone_history = new Ft.TimezoneHistory (); timezone_history.insert (timestamp, this.new_york_timezone); // Destroy one instance to check if entries have been saved and a new instance is // fetching it properly. timezone_history = null; timezone_history = new Ft.TimezoneHistory (); unowned var timezone = timezone_history.search (timestamp); assert_nonnull (timezone); assert_cmpstr ( timezone.get_identifier (), GLib.CompareOperator.EQ, "America/New_York" ); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.TimezoneHistoryTest () ); } focustimerhq-FocusTimer-8581be2/tests/test-utils.vala000066400000000000000000000053141520625676500230060ustar00rootroot00000000000000/* * This file is part of focus-timer * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Kamil Prusko */ namespace Tests { public class AsyncQueueTest : Tests.MainLoopTestSuite { public AsyncQueueTest () { this.add_test ("push", this.test_push); this.add_test ("pop", this.test_pop); this.add_test ("length", this.test_length); this.add_test ("wait", this.test_wait); } public void test_push () { var queue = new Ft.AsyncQueue (); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 0U); queue.push ("a"); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 1U); queue.push ("b"); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 2U); } public void test_pop () { var queue = new Ft.AsyncQueue (); var item = queue.pop (); assert_null (item); queue.push ("x"); item = queue.pop (); assert_nonnull (item); assert_cmpstr (item, GLib.CompareOperator.EQ, "x"); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 0U); } public void test_length () { var queue = new Ft.AsyncQueue (); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 0U); queue.push ("a"); queue.push ("b"); queue.push ("c"); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 3U); var item = queue.pop (); assert_cmpstr (item, GLib.CompareOperator.EQ, "a"); assert_cmpuint (queue.length (), GLib.CompareOperator.EQ, 2U); } public void test_wait () { var queue = new Ft.AsyncQueue (); var completed = false; // Fill queue, then wait for it to become empty queue.push ("a"); queue.push ("b"); queue.wait.begin ((obj, res) => { queue.wait.end (res); completed = true; this.quit_main_loop (); }); // Drain the queue asynchronously to trigger completion GLib.Timeout.add (10, () => { assert_nonnull (queue.pop ()); assert_nonnull (queue.pop ()); return GLib.Source.REMOVE; }); assert_true (this.run_main_loop (1000)); assert_true (completed); } } } public static int main (string[] args) { Tests.init (args); return Tests.run ( new Tests.AsyncQueueTest () ); } focustimerhq-FocusTimer-8581be2/vapi/000077500000000000000000000000001520625676500176165ustar00rootroot00000000000000focustimerhq-FocusTimer-8581be2/vapi/gom-1.0.deps000066400000000000000000000000201520625676500215410ustar00rootroot00000000000000gio-2.0 sqlite3 focustimerhq-FocusTimer-8581be2/vapi/gom-1.0.vapi000066400000000000000000000315721520625676500215650ustar00rootroot00000000000000/* gom-1.0.vapi generated by vapigen, do not modify. */ [CCode (cprefix = "Gom", gir_namespace = "Gom", gir_version = "1.0", lower_case_cprefix = "gom_")] namespace Gom { [CCode (cheader_filename = "gom/gom.h", type_id = "gom_adapter_get_type ()")] public class Adapter : GLib.Object { [CCode (has_construct_function = false)] public Adapter (); public async bool close_async () throws GLib.Error; public bool close_sync () throws GLib.Error; public bool execute_sql (string sql) throws GLib.Error; public unowned Sqlite.Database get_handle (); public async bool open_async (string uri) throws GLib.Error; public bool open_sync (string uri) throws GLib.Error; public void queue_read ([CCode (scope = "async")] Gom.AdapterCallback callback); public void queue_write ([CCode (scope = "async")] Gom.AdapterCallback callback); } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_command_get_type ()")] public class Command : GLib.Object { [CCode (has_construct_function = false)] protected Command (); public bool execute (out unowned Gom.Cursor cursor) throws GLib.Error; public int get_param_index (string param_name); public void reset (); public void set_param (uint param, GLib.Value value); public void set_param_bytes (uint param, GLib.Bytes bytes); public void set_param_double (uint param, double value); public void set_param_float (uint param, float value); public void set_param_int (uint param, int value); public void set_param_int64 (uint param, int64 value); public void set_param_string (uint param, string value); public void set_param_uint (uint param, uint value); public void set_param_uint64 (uint param, uint64 value); public void set_sql (string sql); [NoAccessorMethod] public Gom.Adapter adapter { owned get; construct; } public string sql { set; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_command_builder_get_type ()")] public class CommandBuilder : GLib.Object { [CCode (has_construct_function = false)] protected CommandBuilder (); public Gom.Command build_count (); public GLib.List build_create (uint version); public Gom.Command build_delete (); public Gom.Command build_insert (Gom.Resource resource); public Gom.Command build_select (); public Gom.Command build_update (Gom.Resource resource); [NoAccessorMethod] public Gom.Adapter adapter { owned get; construct; } [NoAccessorMethod] public Gom.Filter filter { owned get; set; } [NoAccessorMethod] public uint limit { get; set; } [NoAccessorMethod] public string m2m_table { owned get; construct; } [NoAccessorMethod] public GLib.Type m2m_type { get; construct; } [NoAccessorMethod] public uint offset { get; set; } [NoAccessorMethod] public GLib.Type resource_type { get; set; } [NoAccessorMethod] public Gom.Sorting sorting { owned get; set; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_cursor_get_type ()")] public class Cursor : GLib.Object { [CCode (has_construct_function = false)] protected Cursor (); public void get_column (uint column, GLib.Value value); public bool get_column_boolean (uint column); public double get_column_double (uint column); public float get_column_float (uint column); public int get_column_int (uint column); public int64 get_column_int64 (uint column); public unowned string get_column_name (uint column); public unowned string get_column_string (uint column); public uint get_column_uint (uint column); public uint64 get_column_uint64 (uint column); public uint get_n_columns (); public bool next (); [NoAccessorMethod] public Sqlite.Statement statement { owned get; construct; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_filter_get_type ()")] public class Filter : GLib.InitiallyUnowned { [CCode (has_construct_function = false)] protected Filter (); [CCode (has_construct_function = false)] public Filter.and (Gom.Filter left, Gom.Filter right); [CCode (cname = "gom_filter_new_and_fullv", has_construct_function = false)] public Filter.and_full ([CCode (array_length = false, type = "GomFilter**")] Gom.Filter[] filter_array); [CCode (has_construct_function = false)] public Filter.eq (GLib.Type resource_type, string property_name, GLib.Value value); public string get_sql (GLib.HashTable table_map); public GLib.Array get_values (); [CCode (has_construct_function = false)] public Filter.glob (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.gt (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.gte (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.is_not_null (GLib.Type resource_type, string property_name); [CCode (has_construct_function = false)] public Filter.is_null (GLib.Type resource_type, string property_name); [CCode (has_construct_function = false)] public Filter.like (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.lt (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.lte (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.neq (GLib.Type resource_type, string property_name, GLib.Value value); [CCode (has_construct_function = false)] public Filter.or (Gom.Filter left, Gom.Filter right); [CCode (cname = "gom_filter_new_or_fullv", has_construct_function = false)] public Filter.or_full ([CCode (array_length = false, type = "GomFilter**")] Gom.Filter[] filter_array); [CCode (has_construct_function = false)] public Filter.sql (string sql, GLib.Array values); [NoAccessorMethod] public Gom.FilterMode mode { get; construct; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_repository_get_type ()")] public class Repository : GLib.Object { [CCode (has_construct_function = false)] public Repository (Gom.Adapter adapter); public async bool automatic_migrate_async (uint version, owned GLib.List object_types) throws GLib.Error; public bool automatic_migrate_sync (uint version, owned GLib.List object_types) throws GLib.Error; public async Gom.ResourceGroup find_async (GLib.Type resource_type, Gom.Filter? filter) throws GLib.Error; public async Gom.Resource find_one_async (GLib.Type resource_type, Gom.Filter? filter) throws GLib.Error; public Gom.Resource find_one_sync (GLib.Type resource_type, Gom.Filter? filter) throws GLib.Error; [CCode (finish_name = "gom_repository_find_finish")] public async Gom.ResourceGroup find_sorted_async (GLib.Type resource_type, Gom.Filter? filter, Gom.Sorting? sorting) throws GLib.Error; public Gom.ResourceGroup find_sorted_sync (GLib.Type resource_type, Gom.Filter? filter, Gom.Sorting? sorting) throws GLib.Error; public Gom.ResourceGroup find_sync (GLib.Type resource_type, Gom.Filter? filter) throws GLib.Error; public unowned Gom.Adapter get_adapter (); public async bool migrate_async (uint version, [CCode (scope = "async")] Gom.RepositoryMigrator migrator) throws GLib.Error; public bool migrate_sync (uint version, Gom.RepositoryMigrator migrator) throws GLib.Error; public Gom.Adapter adapter { get; construct; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_resource_get_type ()")] public abstract class Resource : GLib.Object { [CCode (has_construct_function = false)] protected Resource (); public async bool delete_async () throws GLib.Error; public bool delete_sync () throws GLib.Error; public async Gom.ResourceGroup fetch_m2m_async (GLib.Type resource_type, string m2m_table, Gom.Filter filter) throws GLib.Error; public static GLib.Quark from_bytes_func_quark (); public static bool has_dynamic_pkey (GLib.Type type); public static GLib.Quark new_in_version_quark (); public static GLib.Quark not_mapped_quark (); public static GLib.Quark notnull (); public static GLib.Quark ref_property_name (); public static GLib.Quark ref_table_class (); public async bool save_async () throws GLib.Error; public bool save_sync () throws GLib.Error; [CCode (cname = "gom_resource_class_set_notnull")] public class void set_notnull (string property_name); [CCode (cname = "gom_resource_class_set_primary_key")] public class void set_primary_key (string primary_key); [CCode (cname = "gom_resource_class_set_property_from_bytes")] public class void set_property_from_bytes (string property_name, [CCode (destroy_notify_pos = 2.1)] owned Gom.ResourceFromBytesFunc from_bytes_func); [CCode (cname = "gom_resource_class_set_property_new_in_version")] public class void set_property_new_in_version (string property_name, uint version); [CCode (cname = "gom_resource_class_set_property_set_mapped")] public class void set_property_set_mapped (string property_name, bool is_mapped); [CCode (cname = "gom_resource_class_set_property_to_bytes")] public class void set_property_to_bytes (string property_name, [CCode (destroy_notify_pos = 2.1)] owned Gom.ResourceToBytesFunc to_bytes_func); [CCode (cname = "gom_resource_class_set_property_transform")] public class void set_property_transform (string property_name, [CCode (destroy_notify_pos = 2.1)] owned Gom.ResourceToBytesFunc to_bytes_func, [CCode (destroy_notify_pos = 2.1)] owned Gom.ResourceFromBytesFunc from_bytes_func); [CCode (cname = "gom_resource_class_set_reference")] public class void set_reference (string property_name, string ref_table_name, string ref_property_name); [CCode (cname = "gom_resource_class_set_table")] public class void set_table (string table); [CCode (cname = "gom_resource_class_set_unique")] public class void set_unique (string property_name); public static GLib.Quark to_bytes_func_quark (); public static GLib.Quark unique (); [NoAccessorMethod] public Gom.Repository repository { owned get; set; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_resource_group_get_type ()")] public class ResourceGroup : GLib.Object { [CCode (has_construct_function = false)] public ResourceGroup (Gom.Repository repository); public bool append (Gom.Resource resource); public async bool delete_async () throws GLib.Error; public bool delete_sync () throws GLib.Error; public async bool fetch_async (uint index_, uint count) throws GLib.Error; public bool fetch_sync (uint index_, uint count) throws GLib.Error; public uint get_count (); public unowned Gom.Resource get_index (uint index_); public unowned string get_m2m_table (); public async bool write_async () throws GLib.Error; public bool write_sync () throws GLib.Error; public uint count { get; construct; } [NoAccessorMethod] public Gom.Filter filter { owned get; construct; } [NoAccessorMethod] public bool is_writable { get; construct; } public string m2m_table { get; construct; } [NoAccessorMethod] public GLib.Type m2m_type { get; construct; } [NoAccessorMethod] public Gom.Repository repository { owned get; construct; } [NoAccessorMethod] public GLib.Type resource_type { get; construct; } [NoAccessorMethod] public Gom.Sorting sorting { owned get; construct; } } [CCode (cheader_filename = "gom/gom.h", type_id = "gom_sorting_get_type ()")] public class Sorting : GLib.InitiallyUnowned { [CCode (has_construct_function = false)] protected Sorting (); public void add (GLib.Type resource_type, string property_name, Gom.SortingMode sorting_mode); public string get_sql (GLib.HashTable table_map); } [CCode (cheader_filename = "gom/gom.h", cprefix = "GOM_FILTER_", type_id = "gom_filter_mode_get_type ()")] public enum FilterMode { SQL, OR, AND, EQ, NEQ, GT, GTE, LT, LTE, LIKE, GLOB, IS_NULL, IS_NOT_NULL } [CCode (cheader_filename = "gom/gom.h", cprefix = "GOM_SORTING_", type_id = "gom_sorting_mode_get_type ()")] public enum SortingMode { ASCENDING, DESCENDING } [CCode (cheader_filename = "gom/gom.h", cprefix = "GOM_ERROR_")] public errordomain Error { ADAPTER_OPEN, COMMAND_NO_SQL, COMMAND_SQLITE, REPOSITORY_EMPTY_RESULT, RESOURCE_CURSOR; public static GLib.Quark quark (); } [CCode (cheader_filename = "gom/gom.h", instance_pos = 1.9)] public delegate void AdapterCallback (Gom.Adapter adapter); [CCode (cheader_filename = "gom/gom.h", instance_pos = 3.9)] public delegate bool RepositoryMigrator (Gom.Repository repository, Gom.Adapter adapter, uint version) throws GLib.Error; [CCode (cheader_filename = "gom/gom.h", has_target = false)] public delegate void ResourceFromBytesFunc (GLib.Bytes bytes, GLib.Value value); [CCode (cheader_filename = "gom/gom.h", has_target = false)] public delegate GLib.Bytes ResourceToBytesFunc (GLib.Value value); [CCode (cheader_filename = "gom/gom.h")] [Version (replacement = "Error.quark")] public static GLib.Quark error_quark (); }