init
This commit is contained in:
commit
acb7773889
56
README.md
Normal file
56
README.md
Normal file
@ -0,0 +1,56 @@
|
||||
# DnDBuster
|
||||
|
||||
A timer that notifies you - even if "do not disturb" is enabled.
|
||||
|
||||
## Application IDs
|
||||
|
||||
https://developer.gnome.org/documentation/tutorials/application-id.html
|
||||
|
||||
Used by
|
||||
|
||||
- GtkApplication
|
||||
|
||||
> identifying your application to the system, for ensuring that only one instance of your application is running at a given time, and as a way of passing messages to your application
|
||||
|
||||
- D-Bus
|
||||
|
||||
> to name your application on the message bus. This is the primary means of communicating between applications
|
||||
|
||||
- name of the .desktop file for your application
|
||||
|
||||
> This file is how you describe your application to the system (so that it can be displayed in and launched by GNOME).
|
||||
|
||||
## .desktop entry
|
||||
|
||||
https://wiki.gnome.org/HowDoI/GNotification
|
||||
|
||||
The GNOME desktop environment needs a `.desktop` file that matches your applications ID. Otherwise, sent notifications are not displayed!
|
||||
|
||||
> Warning: gnome-shell uses desktop files to find extra information (app icon, name) about the sender of the notification. If you don't have a desktop file whose base name matches the application id, then your notification will not show up.
|
||||
|
||||
During development, we created a dummy vile at `~/.local/share/applications/de.p1st.dndbuster.desktop` with content:
|
||||
|
||||
```
|
||||
[Desktop Entry]
|
||||
Name=DnDBuster
|
||||
Comment=A simple timer app
|
||||
Exec=steam steam://rungameid/2707930
|
||||
Icon=dndbuster-stopwatch
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Game;
|
||||
```
|
||||
|
||||
Copy `watch-icon.svg` to `~/.local/share/icons/hicolor/scalable/apps/dndbuster-stopwatch.svg`
|
||||
|
||||
## Alternatives
|
||||
|
||||
https://man.archlinux.org/man/notify-send.1.en
|
||||
|
||||
```shell
|
||||
nix-shell -p libnotify
|
||||
```
|
||||
```shell
|
||||
sleep 30m
|
||||
notify-send -u critical -a PleaseDisturbTimer "Alert" "Time is over!"
|
||||
```
|
1
res/source.txt
Normal file
1
res/source.txt
Normal file
@ -0,0 +1 @@
|
||||
https://openclipart.org/detail/20502/pocket-watch-icon
|
73
res/watch-icon.svg
Normal file
73
res/watch-icon.svg
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:cc="http://creativecommons.org/ns#" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:svg="http://www.w3.org/2000/svg" xmlns:ns1="http://sozi.baierouge.fr" id="svg2845" sodipodi:docname="pocket_watch.svg" viewBox="0 0 180 180" sodipodi:version="0.32" version="1.0" inkscape:output_extension="org.inkscape.output.svg.inkscape" inkscape:version="0.46">
|
||||
<sodipodi:namedview id="base" bordercolor="#666666" inkscape:pageshadow="2" inkscape:window-y="0" pagecolor="#ffffff" inkscape:window-height="727" inkscape:zoom="1.76" inkscape:window-x="0" showgrid="false" borderopacity="1.0" inkscape:current-layer="layer2" inkscape:cx="-60.513955" inkscape:cy="68.432376" inkscape:window-width="1280" inkscape:pageopacity="0.0" inkscape:document-units="px"/>
|
||||
<g id="layer1" inkscape:label="background" transform="translate(176.1 -59.114)" inkscape:groupmode="layer" sodipodi:insensitive="true">
|
||||
<rect id="rect2160" style="stroke:#4d4d4d;stroke-width:1.0479;fill:#ccccff" transform="translate(-176.1 59.114)" rx="8.907" ry="8.907" height="174.02" width="174.02" y="2.9904" x="2.9904"/>
|
||||
</g>
|
||||
<g id="layer2" inkscape:label="icon" inkscape:groupmode="layer">
|
||||
<g id="g3588" transform="matrix(1.5037 0 0 1.5037 -114.71 -105.18)">
|
||||
<path id="rect3466" style="stroke-linejoin:round;stroke:#000000;stroke-width:1.5;fill:#000000" d="m127.81 88.812v4.844h4.38v4.844h7.9v-4.844h4.41v-4.844h-16.69z"/>
|
||||
<path id="path3464" sodipodi:rx="47" sodipodi:ry="47" style="stroke-linejoin:round;stroke:#000000;stroke-width:5.2928;fill:#ffff90" sodipodi:type="arc" d="m138 86a47 47 0 1 1 -94 0 47 47 0 1 1 94 0z" transform="matrix(.75575 0 0 .75575 67.372 69.029)" sodipodi:cy="86" sodipodi:cx="91"/>
|
||||
<g id="g3522" transform="matrix(.75575 0 0 .75575 68.127 50.052)">
|
||||
<path id="path3474" style="fill:#000000" d="m89.989 79.487l-3.688 30.443h-0.042l0.042 0.11-0.042 0.3h0.147l3.583 10.17 3.605-10.17h0.147l-0.042-0.3 0.042-0.11h-0.042l-3.71-30.443z"/>
|
||||
<path id="path3480" inkscape:transform-center-x="-10.123637" style="fill:#000000" inkscape:transform-center-y="7.2278985" d="m122.6 134.73l-28.431-24.87 0.025-0.03-0.126-0.04-0.281-0.25-0.086 0.12-12.302-4.39 8.121 10.24-0.086 0.12 0.33 0.18 0.077 0.11 0.025-0.04 32.734 18.85z"/>
|
||||
</g>
|
||||
<g id="g3508" transform="matrix(.75575 0 0 .75575 67.535 49.693)">
|
||||
<path id="path3482" inkscape:transform-center-x="0.70063" style="stroke:#000000;stroke-width:1.3232px;fill:none" inkscape:transform-center-y="-37.718461" d="m90.284 68.851l-0.402 10.031"/>
|
||||
<use id="use3486" inkscape:transform-center-y="-0.70063" inkscape:transform-center-x="-37.71846" xlink:href="#path3482" transform="matrix(0 1 -1 0 202.37 20.802)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3488" xlink:href="#path3482" inkscape:transform-center-x="-32.314828" inkscape:transform-center-y="-19.465994" transform="matrix(.5 .86603 -.86603 .5 142.03 -22.828)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3490" inkscape:transform-center-y="-33.015459" inkscape:transform-center-x="-18.252464" xlink:href="#path3482" transform="matrix(.86603 .5 -.5 .86603 67.955 -30.442)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3492" xlink:href="#path3482" inkscape:transform-center-x="-33.015458" inkscape:transform-center-y="18.252464" transform="matrix(-.5 .86603 -.86603 -.5 232.81 88.757)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3494" inkscape:transform-center-y="32.314827" inkscape:transform-center-x="-19.465989" xlink:href="#path3482" transform="matrix(-.86603 .5 -.5 -.86603 225.2 162.83)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3496" xlink:href="#path3482" inkscape:transform-center-x="-0.70063083" inkscape:transform-center-y="37.718459" transform="matrix(-1 0 0 -1 181.57 223.17)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3498" inkscape:transform-center-y="33.015457" inkscape:transform-center-x="18.252461" xlink:href="#path3482" transform="matrix(-.86603 -.5 .5 -.86603 113.61 253.61)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3500" xlink:href="#path3482" inkscape:transform-center-x="32.314824" inkscape:transform-center-y="19.465998" transform="matrix(-.5 -.86603 .86603 -.5 39.54 246)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3502" inkscape:transform-center-y="0.70063983" inkscape:transform-center-x="37.718457" xlink:href="#path3482" transform="matrix(0 -1 1 0 -20.802 202.37)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3504" xlink:href="#path3482" inkscape:transform-center-x="33.015463" inkscape:transform-center-y="-18.252452" transform="matrix(.5 -.86603 .86603 .5 -51.244 134.41)" height="180" width="180" y="0" x="0"/>
|
||||
<use id="use3506" inkscape:transform-center-y="-32.314819" inkscape:transform-center-x="19.466004" xlink:href="#path3482" transform="matrix(.86603 -.5 .5 .86603 -43.63 60.341)" height="180" width="180" y="0" x="0"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<metadata>
|
||||
<rdf:RDF>
|
||||
<cc:Work>
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<cc:license rdf:resource="http://creativecommons.org/licenses/publicdomain/"/>
|
||||
<dc:publisher>
|
||||
<cc:Agent rdf:about="http://openclipart.org/">
|
||||
<dc:title>Openclipart</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:publisher>
|
||||
<dc:title>Pocket watch icon</dc:title>
|
||||
<dc:date>2008-12-01T15:14:04</dc:date>
|
||||
<dc:description>Pocket watch icon Originally developed for www.studenti.unige.it</dc:description>
|
||||
<dc:source>https://openclipart.org/detail/20502/pocket-watch-icon-by-cod_fsfe</dc:source>
|
||||
<dc:creator>
|
||||
<cc:Agent>
|
||||
<dc:title>CoD_fsfe</dc:title>
|
||||
</cc:Agent>
|
||||
</dc:creator>
|
||||
<dc:subject>
|
||||
<rdf:Bag>
|
||||
<rdf:li>clock</rdf:li>
|
||||
<rdf:li>color</rdf:li>
|
||||
<rdf:li>icon</rdf:li>
|
||||
<rdf:li>inkscape</rdf:li>
|
||||
<rdf:li>pocket watch</rdf:li>
|
||||
<rdf:li>time</rdf:li>
|
||||
<rdf:li>watch</rdf:li>
|
||||
<rdf:li>web</rdf:li>
|
||||
<rdf:li>webdesign</rdf:li>
|
||||
</rdf:Bag>
|
||||
</dc:subject>
|
||||
</cc:Work>
|
||||
<cc:License rdf:about="http://creativecommons.org/licenses/publicdomain/">
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Reproduction"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#Distribution"/>
|
||||
<cc:permits rdf:resource="http://creativecommons.org/ns#DerivativeWorks"/>
|
||||
</cc:License>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<script xmlns=""/></svg>
|
After Width: | Height: | Size: 6.9 KiB |
19
shell.nix
Normal file
19
shell.nix
Normal file
@ -0,0 +1,19 @@
|
||||
{ pkgs ? import <nixpkgs> {} }:
|
||||
pkgs.mkShell {
|
||||
buildInputs = [
|
||||
(pkgs.python3.withPackages (python-pkgs: with python-pkgs; [
|
||||
# Python bindings for Glib
|
||||
pygobject3
|
||||
# Typing Stubs for PyGObject
|
||||
pygobject-stubs
|
||||
|
||||
# Not sure if required
|
||||
#dbus-python
|
||||
]))
|
||||
|
||||
# Not sure if required
|
||||
#pkgs.glib
|
||||
#pkgs.gtk4
|
||||
#pkgs.gobject-introspection
|
||||
];
|
||||
}
|
115
src/dndbuster/app.py
Normal file
115
src/dndbuster/app.py
Normal file
@ -0,0 +1,115 @@
|
||||
import gi
|
||||
|
||||
from timer import Timer
|
||||
|
||||
gi.require_version('Gtk', '4.0')
|
||||
gi.require_version('Adw', '1')
|
||||
from gi.repository import GLib, Gio, Gtk, Adw
|
||||
|
||||
|
||||
def main():
|
||||
# Create a new application
|
||||
app = Application()
|
||||
# Run the application
|
||||
app.run(None)
|
||||
|
||||
|
||||
class Application(Adw.Application):
|
||||
def __init__(self):
|
||||
super().__init__(application_id='de.p1st.dndbuster')
|
||||
self.window: AppWindow | None = None
|
||||
|
||||
def do_activate(self):
|
||||
# Windows are associated with the application.
|
||||
# When the last one is closed the application shuts down.
|
||||
self.window = AppWindow(application=self)
|
||||
self.window.present()
|
||||
|
||||
def notify_timer(self):
|
||||
notification = Gio.Notification()
|
||||
notification.set_title('Timeout')
|
||||
notification.set_body('The time is over and your do-not-disturb mode just got busted!')
|
||||
notification.set_priority(Gio.NotificationPriority.URGENT)
|
||||
self.send_notification(id=f'{self.get_application_id()}.{Timer.current_time()}', notification=notification)
|
||||
|
||||
|
||||
class AppWindow(Gtk.ApplicationWindow):
|
||||
def __init__(self, application: Application):
|
||||
super().__init__(application=application, title='DnDBuster')
|
||||
|
||||
self.app = application
|
||||
self.set_default_size(400, 200)
|
||||
|
||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.box.set_spacing(10)
|
||||
self.box.set_margin_top(10)
|
||||
self.box.set_margin_bottom(10)
|
||||
self.box.set_margin_start(10)
|
||||
self.box.set_margin_end(10)
|
||||
self.set_child(self.box)
|
||||
|
||||
self.button_start_restart = Gtk.Button(label='Start')
|
||||
self.button_start_restart.connect('clicked', self.on_button_start_restart_clicked)
|
||||
self.box.append(self.button_start_restart)
|
||||
|
||||
self.button_pause_resume = Gtk.Button(label='Pause', visible=False)
|
||||
self.button_pause_resume.connect('clicked', self.on_button_pause_resume_clicked)
|
||||
self.box.append(self.button_pause_resume)
|
||||
|
||||
self.progress_bar = Gtk.ProgressBar(visible=False)
|
||||
self.box.append(self.progress_bar)
|
||||
|
||||
self.timer = None
|
||||
|
||||
def update_progress(self) -> bool:
|
||||
total_min = 25
|
||||
total_sec = total_min * 60
|
||||
delta_sec = self.timer.read()
|
||||
progress = delta_sec / total_sec
|
||||
self.progress_bar.set_fraction(progress)
|
||||
|
||||
if progress >= 1:
|
||||
self.app.notify_timer()
|
||||
# Stop regularly calling update_progress()
|
||||
return False
|
||||
|
||||
# Weather to continue regularly calling update_progress()
|
||||
return not self.timer.is_paused()
|
||||
|
||||
def on_button_start_restart_clicked(self, _widget):
|
||||
if self.timer is None:
|
||||
self.add_timer()
|
||||
|
||||
self.button_start_restart.set_label("Restart")
|
||||
self.button_pause_resume.set_visible(True)
|
||||
else:
|
||||
self.add_timer()
|
||||
|
||||
self.update_label()
|
||||
|
||||
def add_timer(self):
|
||||
self.timer = Timer()
|
||||
self.timer.start()
|
||||
self.progress_bar.set_visible(True)
|
||||
self.add_timeout()
|
||||
|
||||
def add_timeout(self):
|
||||
GLib.timeout_add_seconds(1, self.update_progress)
|
||||
|
||||
def on_button_pause_resume_clicked(self, _widget):
|
||||
if self.timer.is_paused():
|
||||
self.timer.resume()
|
||||
self.add_timeout()
|
||||
else:
|
||||
self.timer.pause()
|
||||
self.update_label()
|
||||
|
||||
def update_label(self):
|
||||
if self.timer.is_paused():
|
||||
self.button_pause_resume.set_label("Resume")
|
||||
else:
|
||||
self.button_pause_resume.set_label("Pause")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
58
src/dndbuster/timer.py
Normal file
58
src/dndbuster/timer.py
Normal file
@ -0,0 +1,58 @@
|
||||
import time
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self):
|
||||
self.start_seconds = None
|
||||
self.pause_timer = None
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Start time measurement
|
||||
"""
|
||||
self.start_seconds = self.current_time()
|
||||
|
||||
|
||||
def is_started(self) -> bool:
|
||||
"""
|
||||
:return: Weather time measurement has been started.
|
||||
"""
|
||||
return self.start_seconds is not None
|
||||
|
||||
def pause(self):
|
||||
"""
|
||||
Pause time measurement.
|
||||
"""
|
||||
self.pause_timer = Timer()
|
||||
self.pause_timer.start()
|
||||
|
||||
def is_paused(self):
|
||||
"""
|
||||
:return: Weather time measurement is paused.
|
||||
"""
|
||||
return self.pause_timer is not None
|
||||
|
||||
def resume(self):
|
||||
"""
|
||||
Resumes time measurement.
|
||||
"""
|
||||
if self.pause_timer:
|
||||
pause_delta = self.pause_timer.read()
|
||||
self.start_seconds += pause_delta
|
||||
self.pause_timer = None
|
||||
|
||||
def read(self):
|
||||
"""
|
||||
Return measured time in seconds.
|
||||
"""
|
||||
end_seconds = self.current_time()
|
||||
delta = end_seconds - self.start_seconds
|
||||
|
||||
if self.is_paused():
|
||||
delta -= self.pause_timer.read()
|
||||
|
||||
return delta
|
||||
|
||||
@classmethod
|
||||
def current_time(cls):
|
||||
return time.time()
|
Loading…
x
Reference in New Issue
Block a user