From 1b3121d05c8df54d984eb9c15ad42bb9674f05bf Mon Sep 17 00:00:00 2001 From: TobiZog <67568441+TobiZog@users.noreply.github.com> Date: Sun, 4 Feb 2024 16:38:19 +0100 Subject: [PATCH] cinnamon-dynamic-wallpaper@TobiZog: Cinnamon Dynamic Wallpaper v2.2 (#559) --- cinnamon-dynamic-wallpaper@TobiZog/CHANGELOG | 10 + cinnamon-dynamic-wallpaper@TobiZog/README.md | 6 +- .../5.4/extension.js | 5 +- .../5.4/icon.svg | 2 +- .../5.4/loop.py | 110 --- .../5.4/preferences.py | 642 --------------- .../5.4/{ => res}/icons/icon.png | Bin .../5.4/{ => res}/icons/icon.svg | 0 .../res/images/dynamic_background_example.png | Bin 0 -> 42789 bytes .../images/included_image_sets/aurora/0.jpg | Bin .../images/included_image_sets/aurora/1.jpg | Bin .../images/included_image_sets/aurora/2.jpg | Bin .../images/included_image_sets/aurora/3.jpg | Bin .../images/included_image_sets/aurora/4.jpg | Bin .../images/included_image_sets/aurora/5.jpg | Bin .../images/included_image_sets/aurora/6.jpg | Bin .../images/included_image_sets/aurora/7.jpg | Bin .../images/included_image_sets/aurora/8.jpg | Bin .../images/included_image_sets/aurora/9.jpg | Bin .../images/included_image_sets/beach/0.jpg | Bin .../images/included_image_sets/beach/1.jpg | Bin .../images/included_image_sets/beach/2.jpg | Bin .../images/included_image_sets/beach/3.jpg | Bin .../images/included_image_sets/beach/4.jpg | Bin .../images/included_image_sets/beach/5.jpg | Bin .../images/included_image_sets/beach/6.jpg | Bin .../images/included_image_sets/beach/7.jpg | Bin .../images/included_image_sets/beach/8.jpg | 0 .../images/included_image_sets/beach/9.jpg | 0 .../images/included_image_sets/bitday/0.jpg | Bin .../images/included_image_sets/bitday/1.jpg | Bin .../images/included_image_sets/bitday/2.jpg | Bin .../images/included_image_sets/bitday/3.jpg | Bin .../images/included_image_sets/bitday/4.jpg | Bin .../images/included_image_sets/bitday/5.jpg | Bin .../images/included_image_sets/bitday/6.jpg | Bin .../images/included_image_sets/bitday/7.jpg | Bin .../images/included_image_sets/bitday/8.jpg | Bin .../images/included_image_sets/bitday/9.png | Bin .../images/included_image_sets/cliffs/0.jpg | Bin .../images/included_image_sets/cliffs/1.jpg | Bin .../images/included_image_sets/cliffs/2.jpg | Bin .../images/included_image_sets/cliffs/3.jpg | Bin .../images/included_image_sets/cliffs/4.jpg | Bin .../images/included_image_sets/cliffs/5.jpg | 0 .../images/included_image_sets/cliffs/6.jpg | Bin .../images/included_image_sets/cliffs/7.jpg | Bin .../images/included_image_sets/cliffs/8.jpg | Bin .../images/included_image_sets/cliffs/9.jpg | 0 .../images/included_image_sets/earth/0.jpg | Bin .../images/included_image_sets/earth/1.jpg | Bin .../images/included_image_sets/earth/2.jpg | Bin .../images/included_image_sets/earth/3.jpg | Bin .../images/included_image_sets/earth/4.jpg | Bin .../images/included_image_sets/earth/5.jpg | Bin .../images/included_image_sets/earth/6.jpg | Bin .../images/included_image_sets/earth/7.jpg | Bin .../images/included_image_sets/earth/8.jpg | Bin .../images/included_image_sets/earth/9.jpg | Bin .../images/included_image_sets/gradient/0.jpg | Bin .../images/included_image_sets/gradient/1.jpg | Bin .../images/included_image_sets/gradient/2.jpg | Bin .../images/included_image_sets/gradient/3.jpg | Bin .../images/included_image_sets/gradient/4.jpg | Bin .../images/included_image_sets/gradient/5.jpg | Bin .../images/included_image_sets/gradient/6.jpg | Bin .../images/included_image_sets/gradient/7.jpg | Bin .../images/included_image_sets/gradient/8.jpg | Bin .../images/included_image_sets/gradient/9.png | Bin .../images/included_image_sets/lakeside/0.jpg | Bin .../images/included_image_sets/lakeside/1.jpg | Bin .../images/included_image_sets/lakeside/2.jpg | Bin .../images/included_image_sets/lakeside/3.jpg | Bin .../images/included_image_sets/lakeside/4.jpg | Bin .../images/included_image_sets/lakeside/5.jpg | Bin .../images/included_image_sets/lakeside/6.jpg | Bin .../images/included_image_sets/lakeside/7.jpg | Bin .../images/included_image_sets/lakeside/8.jpg | Bin .../images/included_image_sets/lakeside/9.jpg | Bin .../included_image_sets/mountains/0.jpg | Bin .../included_image_sets/mountains/1.jpg | Bin .../included_image_sets/mountains/2.jpg | Bin .../included_image_sets/mountains/3.jpg | Bin .../included_image_sets/mountains/4.jpg | Bin .../included_image_sets/mountains/5.jpg | Bin .../included_image_sets/mountains/6.jpg | Bin .../included_image_sets/mountains/7.jpg | Bin .../included_image_sets/mountains/8.jpg | Bin .../included_image_sets/mountains/9.jpg | 0 .../images/included_image_sets/sahara/0.jpg | Bin .../images/included_image_sets/sahara/1.jpg | Bin .../images/included_image_sets/sahara/2.jpg | Bin .../images/included_image_sets/sahara/3.jpg | Bin .../images/included_image_sets/sahara/4.jpg | Bin .../images/included_image_sets/sahara/5.jpg | Bin .../images/included_image_sets/sahara/6.jpg | Bin .../images/included_image_sets/sahara/7.jpg | Bin .../images/included_image_sets/sahara/8.jpg | Bin .../images/included_image_sets/sahara/9.jpg | Bin .../5.4/{ => res}/preferences.glade | 133 ++- .../5.4/scripts/cinnamon_pref_handler.py | 100 --- .../5.4/scripts/display.py | 20 - .../5.4/scripts/location.py | 22 - .../5.4/scripts/ui.py | 147 ---- .../5.4/settings-schema.json | 4 + .../5.4/{ => src}/enums/ImageSourceEnum.py | 0 .../5.4/src/enums/NetworkLocationProvider.py | 4 + .../5.4/{ => src}/enums/PeriodSourceEnum.py | 0 .../5.4/src/main.py | 17 + .../5.4/src/model/main_view_model.py | 259 ++++++ .../5.4/src/service/cinnamon_pref_handler.py | 132 +++ .../5.4/{scripts => src/service}/images.py | 0 .../5.4/src/service/location.py | 39 + .../5.4/{scripts => src/service}/suntimes.py | 4 - .../service}/time_bar_chart.py | 4 +- .../5.4/{scripts => src/view}/dialogs.py | 7 +- .../5.4/src/view/main_window.py | 754 ++++++++++++++++++ .../icon.png | 2 +- .../icon.svg | 2 +- .../metadata.json | 6 +- 120 files changed, 1353 insertions(+), 1078 deletions(-) delete mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/loop.py delete mode 100755 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/preferences.py rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/icons/icon.png (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/icons/icon.svg (100%) create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/images/dynamic_background_example.png rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/aurora/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/beach/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/bitday/9.png (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/cliffs/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/earth/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/gradient/9.png (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/lakeside/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/mountains/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/0.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/1.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/2.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/3.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/4.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/5.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/6.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/7.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/8.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/images/included_image_sets/sahara/9.jpg (100%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => res}/preferences.glade (96%) delete mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/cinnamon_pref_handler.py delete mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/display.py delete mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/location.py delete mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/ui.py rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => src}/enums/ImageSourceEnum.py (100%) create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/NetworkLocationProvider.py rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{ => src}/enums/PeriodSourceEnum.py (100%) create mode 100755 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/main.py create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/model/main_view_model.py create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/cinnamon_pref_handler.py rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{scripts => src/service}/images.py (100%) create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/location.py rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{scripts => src/service}/suntimes.py (97%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{scripts => src/service}/time_bar_chart.py (97%) rename cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/{scripts => src/view}/dialogs.py (88%) create mode 100644 cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/main_window.py diff --git a/cinnamon-dynamic-wallpaper@TobiZog/CHANGELOG b/cinnamon-dynamic-wallpaper@TobiZog/CHANGELOG index 99cb7946..5a7d60c8 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/CHANGELOG +++ b/cinnamon-dynamic-wallpaper@TobiZog/CHANGELOG @@ -1,3 +1,13 @@ +# Version 2.2 +- Bugfixes +- Migrate code to MVVM pattern +- Adding option to change the location provider +- Adding example image for dynamic background color + +# Version 2.1 +- Bugfixes +- Smaller UI for displays with reduced resolution (< 1000px height) + # Version 2.0 - New App icon - Preferences window redesign - All settings are now in one window accessable! diff --git a/cinnamon-dynamic-wallpaper@TobiZog/README.md b/cinnamon-dynamic-wallpaper@TobiZog/README.md index 51848bba..1860e778 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/README.md +++ b/cinnamon-dynamic-wallpaper@TobiZog/README.md @@ -7,11 +7,11 @@ This extension switches the background image of your Cinnamon desktop multiple t - 10 day periods - HEIF converter - Image configuration assistent with simple one-click setup for image choices -- Online location estimation or offline with manual latitude and longitude input +- Online location estimation (three provider) or offline with manual latitude and longitude input - Time periods individual configured by user - Offline sun angles estimation - Image stretching over multiple displays or repeat image for every display -- Show image on lock screen +- Creating a color gradient based on the current wallpaper for images which not fill the whole screen ### Tested Cinnamon versions - 5.4 (Mint 21) @@ -37,7 +37,7 @@ This extension switches the background image of your Cinnamon desktop multiple t 3. Search and download it ### From the repo -1. Download the Repository +1. Download the latest from the Releases page on GitHub: https://github.com/TobiZog/cinnamon-dynamic-wallpaper/releases 2. Extract the files 3. Copy the folder `cinnamon-dynamic-wallpaper@TobiZog` to `~/.local/share/cinnamon/extensions/` --- diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/extension.js b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/extension.js index 0fd8ab68..ee7477d2 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/extension.js +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/extension.js @@ -96,7 +96,7 @@ CinnamonDynamicWallpaperExtension.prototype = { _loop: function () { if (looping) { try { - Util.spawnCommandLine("/usr/bin/env python3 " + DIRECTORY.path + "/loop.py") + Util.spawnCommandLine("/usr/bin/env python3 " + DIRECTORY.path + "/src/main.py loop") } catch(e) { this.showNotification("Error!", "Cinnamon Dynamic Wallpaper got an error while running the loop script. Please create an issue on GitHub.") @@ -127,8 +127,7 @@ CinnamonDynamicWallpaperExtension.prototype = { notification.addButton("open-settings", _("Open settings")); notification.connect("action-invoked", () => - Util.spawnCommandLine("/usr/bin/env python3 " + - DIRECTORY.path + "/preferences.py")); + Util.spawnCommandLine("/usr/bin/env python3 " + DIRECTORY.path + "/src/main.py")); } // Put all together diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icon.svg b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icon.svg index 81b66344..1f302657 120000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icon.svg +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icon.svg @@ -1 +1 @@ -icons/icon.svg \ No newline at end of file +res/icons/icon.svg \ No newline at end of file diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/loop.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/loop.py deleted file mode 100644 index 916e6e45..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/loop.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/python3 - -from scripts.cinnamon_pref_handler import * -from scripts.suntimes import * -from datetime import datetime, time -from enums.PeriodSourceEnum import * -from scripts.location import * -from gi.repository import Gio -from PIL import Image - - -class Loop(): - def __init__(self) -> None: - self.prefs = Cinnamon_Pref_Handler() - - self.suntimes = Suntimes() - self.location = Location() - self.background_settings = Gio.Settings.new("org.cinnamon.desktop.background") - - # Position should estimate by network - if self.prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: - current_location = self.location.get_location() - - self.suntimes.calc_suntimes(float(current_location["latitude"]), float(current_location["longitude"])) - self.start_times = self.suntimes.day_periods - - # Position is given by user - elif self.prefs.period_source == PeriodSourceEnum.CUSTOMLOCATION: - self.suntimes.calc_suntimes(float(self.prefs.latitude_custom), float(self.prefs.longitude_custom)) - self.start_times = self.suntimes.day_periods - - # No position, concrete times - else: - def string_to_time_converter(raw_str: str) -> time: - hour = raw_str[0:raw_str.find(":")] - minute = raw_str[raw_str.find(":") + 1:] - - return time(hour=int(hour), minute=int(minute)) - - self.start_times = [ - string_to_time_converter(self.prefs.period_custom_start_time[0]), - string_to_time_converter(self.prefs.period_custom_start_time[1]), - string_to_time_converter(self.prefs.period_custom_start_time[2]), - string_to_time_converter(self.prefs.period_custom_start_time[3]), - string_to_time_converter(self.prefs.period_custom_start_time[4]), - string_to_time_converter(self.prefs.period_custom_start_time[5]), - string_to_time_converter(self.prefs.period_custom_start_time[6]), - string_to_time_converter(self.prefs.period_custom_start_time[7]), - string_to_time_converter(self.prefs.period_custom_start_time[8]), - string_to_time_converter(self.prefs.period_custom_start_time[9]) - ] - - - def exchange_image(self): - """ Replace the desktop image - """ - # Get the time of day - time_now = time(datetime.now().hour, datetime.now().minute) - - # Assign the last image as fallback - self.current_image_uri = self.prefs.source_folder + self.prefs.period_images[9] - - for i in range(0, 9): - # Replace the image URI, if it's not the last time period of the day - if self.start_times[i] <= time_now and time_now < self.start_times[i + 1]: - self.current_image_uri = self.prefs.source_folder + self.prefs.period_images[i] - break - - # Set the background - self.background_settings['picture-uri'] = "file://" + self.current_image_uri - - # Set background stretching - self.background_settings['picture-options'] = self.prefs.picture_aspect - - self.set_background_gradient() - - - def set_background_gradient(self): - """ Setting a gradient background to hide images, which are not high enough - """ - # Load the image - try: - im = Image.open(self.current_image_uri) - pix = im.load() - - # Width and height of the current setted image - width, height = im.size - - # Color of the top and bottom pixel in the middle of the image - top_color = pix[width / 2,0] - bottom_color = pix[width / 2, height - 1] - - # Create the gradient - self.background_settings['color-shading-type'] = "vertical" - - if self.prefs.dynamic_background_color: - self.background_settings['primary-color'] = f"#{top_color[0]:x}{top_color[1]:x}{top_color[2]:x}" - self.background_settings['secondary-color'] = f"#{bottom_color[0]:x}{bottom_color[1]:x}{bottom_color[2]:x}" - else: - self.background_settings['primary-color'] = "#000000" - self.background_settings['secondary-color'] = "#000000" - except: - self.background_settings['primary-color'] = "#000000" - self.background_settings['secondary-color'] = "#000000" - - -# Needed for JavaScript -if __name__ == "__main__": - l = Loop() - l.exchange_image() diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/preferences.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/preferences.py deleted file mode 100755 index 80f1a071..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/preferences.py +++ /dev/null @@ -1,642 +0,0 @@ -#!/usr/bin/python3 - -############################################################ -# Imports # -############################################################ - -# Packages -import gi, os, subprocess, time -from datetime import timedelta - -# Local scripts -from scripts import cinnamon_pref_handler, dialogs, display, images, location, suntimes, time_bar_chart, ui -from loop import * -from enums.ImageSourceEnum import ImageSourceEnum -from enums.PeriodSourceEnum import PeriodSourceEnum - -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk, GdkPixbuf - - -# Global definitions -PREFERENCES_URI = os.path.dirname(os.path.abspath(__file__)) -GLADE_URI = PREFERENCES_URI + "/preferences.glade" - - -class Preferences: - """ Preference window class - """ - - ############################################################ - # Lifecycle # - ############################################################ - - def __init__(self) -> None: - # Objects from external scripts - self.prefs = cinnamon_pref_handler.Cinnamon_Pref_Handler() - self.dialogs = dialogs.Dialogs() - self.display = display.Display() - self.images = images.Images() - self.location = location.Location() - self.suntimes = suntimes.Suntimes() - self.time_bar_chart = time_bar_chart.Time_Bar_Chart() - - # Glade - self.builder = Gtk.Builder() - self.builder.add_from_file(GLADE_URI) - self.builder.connect_signals(self) - - self.ui = ui.UI(self.builder) - - - # Local Config - self.smaller_ui_height = 1000 - - - def show(self): - """ Display the window to the screen - """ - window = self.builder.get_object("window_main") - window.show_all() - - # Load from preferences - if self.prefs.image_source == ImageSourceEnum.IMAGESET: - self.ui.tb_image_set.set_active(True) - elif self.prefs.image_source == ImageSourceEnum.HEICFILE: - self.ui.tb_heic_file.set_active(True) - elif self.prefs.image_source == ImageSourceEnum.SOURCEFOLDER: - self.ui.tb_source_folder.set_active(True) - - - # Remove icons in the ToggleButtons if the screen height resolution < 1000 px - if self.display.get_screen_height() < self.smaller_ui_height: - self.ui.img_tb_image_set.clear() - self.ui.img_tb_heic_file.clear() - self.ui.img_tb_source_folder.clear() - self.ui.img_tb_network_location.clear() - self.ui.img_tb_custom_location.clear() - self.ui.img_tb_time_periods.clear() - - - picture_aspects = ["centered", "scaled", "stretched", "zoom", "spanned"] - self.ui.add_items_to_combo_box(self.ui.cb_picture_aspect, picture_aspects) - self.ui.set_active_combobox_item(self.ui.cb_picture_aspect, self.prefs.picture_aspect) - - self.ui.sw_dynamic_background_color.set_active(self.prefs.dynamic_background_color) - - - if self.prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: - self.ui.tb_network_location.set_active(True) - elif self.prefs.period_source == PeriodSourceEnum.CUSTOMLOCATION: - self.ui.tb_custom_location.set_active(True) - elif self.prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: - self.ui.tb_time_periods.set_active(True) - - - # Time diagram - try: - self.refresh_chart() - except: - pass - - # Show the main window - Gtk.main() - - - def on_destroy(self, *args): - """ Lifecycle handler when window will be destroyed - """ - Gtk.main_quit() - - - - ############################################################ - # Local methods # - ############################################################ - - def refresh_chart(self): - """ Recomputes both time bar charts and load them to the UI - """ - # Stores the start times of the periods in minutes since midnight - time_periods_min = [] - - if self.prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: - for i in range(0, 10): - time_str = self.prefs.period_custom_start_time[i] - - time_periods_min.append(int(time_str[0:2]) * 60 + int(time_str[3:5])) - else: - if self.prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: - self.suntimes.calc_suntimes(float(self.prefs.latitude_auto), float(self.prefs.longitude_auto)) - else: - self.suntimes.calc_suntimes(float(self.ui.etr_latitude.get_text()), float(self.ui.etr_longitude.get_text())) - - - # Get all time periods. Store the minutes to the list and print the values to the text views - for i in range(0, 10): - time_range_now = self.suntimes.day_periods[i] - - if i != 9: - time_range_next = self.suntimes.day_periods[i + 1] - else: - time_range_next = time(hour=23, minute=59) - - self.ui.etr_periods[i].set_text( - str(time_range_now.hour).rjust(2, '0') + ":" + str(time_range_now.minute).rjust(2, '0') + \ - " - " + str(time_range_next.hour).rjust(2, '0') + ":" + str(time_range_next.minute).rjust(2, '0')) - - time_periods_min.append(time_range_now.hour * 60 + time_range_now.minute) - - # Create time bar - # Reduce size for small displays - if self.display.get_screen_height() < self.smaller_ui_height: - bar_width = 1150 - bar_height = 110 - else: - bar_width = 1300 - bar_height = 150 - - self.time_bar_chart.create_bar_chart_with_polylines(PREFERENCES_URI, bar_width, bar_height, time_periods_min) - self.time_bar_chart.create_bar_chart(PREFERENCES_URI, bar_width, bar_height, time_periods_min) - - # Load to the views - pixbuf = GdkPixbuf.Pixbuf.new_from_file(PREFERENCES_URI + "/time_bar_polylines.svg") - self.ui.img_bar_images.set_from_pixbuf(pixbuf) - - pixbuf2 = GdkPixbuf.Pixbuf.new_from_file(PREFERENCES_URI + "/time_bar.svg") - self.ui.img_bar_times.set_from_pixbuf(pixbuf2) - - - def load_image_options_to_combo_boxes(self, options: list): - """ Add a list of Strings to all image option comboboxes - - Args: - options (list): All possible options - """ - options.insert(0, "") - - for combobox in self.ui.cb_periods: - self.ui.add_items_to_combo_box(combobox, options) - - - def load_image_to_preview(self, image_preview: Gtk.Image, image_src: str): - """ Scales the image to a lower resoultion and put them into the time bar chart - - Args: - image_preview (Gtk.Image): Gtk Image where it will be displayed - image_src (str): Absolute path to the image - """ - try: - pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_src) - - screen_height = self.display.get_screen_height() - - # Scaling the images smaller for screens - if screen_height < self.smaller_ui_height: - pixbuf = pixbuf.scale_simple(221, 128, GdkPixbuf.InterpType.BILINEAR) - else: - pixbuf = pixbuf.scale_simple(260, 150, GdkPixbuf.InterpType.BILINEAR) - - image_preview.set_from_pixbuf(pixbuf) - except: - pass - - - ############################################################ - # Callbacks # - ############################################################ - - ## Image Configuration - - # +-----------+-----------+---------------+ - # | Image Set | HEIC file | Source Folder | - # +-----------+-----------+---------------+ - - def on_toggle_button_image_set_clicked(self, button: Gtk.ToggleButton): - """ Clicked on ToggleButton "Image Set" - - Args: - button (Gtk.ToggleButton): Clicked ToggleButton - """ - if button.get_active(): - self.prefs.image_source = ImageSourceEnum.IMAGESET - self.ui.tb_heic_file.set_active(False) - self.ui.tb_source_folder.set_active(False) - - self.ui.lbr_image_set.set_visible(True) - self.ui.lbr_heic_file.set_visible(False) - self.ui.lbr_source_folder.set_visible(False) - - image_set_choices = [ - "aurora", "beach", - "bitday", "cliffs", - "earth", "gradient", - "lakeside", "mountains", - "sahara" - ] - self.ui.add_items_to_combo_box(self.ui.cb_image_set, image_set_choices) - - self.ui.set_active_combobox_item(self.ui.cb_image_set, self.prefs.selected_image_set) - - for i, combobox in enumerate(self.ui.cb_periods): - selected_image_name = self.prefs.period_images[i] - self.ui.set_active_combobox_item(combobox, selected_image_name) - - # Make the comboboxes invisible - for combobox in self.ui.cb_periods: - combobox.set_visible(False) - - - def on_toggle_button_heic_file_clicked(self, button: Gtk.ToggleButton): - """ Clicked on ToggleButton "Heic file" - - Args: - button (Gtk.ToggleButton): Clicked ToggleButton - """ - if button.get_active(): - self.prefs.image_source = ImageSourceEnum.HEICFILE - self.ui.tb_image_set.set_active(False) - self.ui.tb_source_folder.set_active(False) - - self.ui.lbr_image_set.set_visible(False) - self.ui.lbr_heic_file.set_visible(True) - self.ui.lbr_source_folder.set_visible(False) - - # Make the comboboxes visible - for combobox in self.ui.cb_periods: - combobox.set_visible(True) - - # Load images from source folder - files = self.images.get_images_from_folder(self.prefs.source_folder) - - if len(files) != 0: - self.load_image_options_to_combo_boxes(files) - - # Load the values for the images from the preferences - for i in range(0, 10): - self.ui.set_active_combobox_item(self.ui.cb_periods[i], self.prefs.period_images[i]) - else: - print("No image files!") - - - def on_toggle_button_source_folder_clicked(self, button: Gtk.ToggleButton): - """ Clicked on ToggleButton "Source Folder" - - Args: - button (Gtk.ToggleButton): Clicked ToggleButton - """ - if button.get_active(): - self.prefs.image_source = ImageSourceEnum.SOURCEFOLDER - self.ui.tb_image_set.set_active(False) - self.ui.tb_heic_file.set_active(False) - - self.ui.lbr_image_set.set_visible(False) - self.ui.lbr_heic_file.set_visible(False) - self.ui.lbr_source_folder.set_visible(True) - - # Make the comboboxes visible - for combobox in self.ui.cb_periods: - combobox.set_visible(True) - - # Load the source folder to the view - # This will update the comboboxes in the preview to contain the right items - self.ui.lbl_source_folder.set_label(self.prefs.source_folder) - - # Load files from saved source folder - files = self.images.get_images_from_folder(self.prefs.source_folder) - - if len(files) != 0: - self.load_image_options_to_combo_boxes(files) - - # Load the values for the images from the preferences - for i in range(0, 10): - self.ui.set_active_combobox_item(self.ui.cb_periods[i], self.prefs.period_images[i]) - else: - print("No image files!") - - - - # +------------------------------------+ - # | Select an image set | aurora ▼ | - # +------------------------------------+ - - def on_cb_image_set_changed(self, combobox: Gtk.ComboBox): - """ User select on of the included image sets - - Args: - combobox (Gtk.ComboBox): The used ComboBox - """ - tree_iter = combobox.get_active_iter() - - if tree_iter is not None and self.prefs.image_source == ImageSourceEnum.IMAGESET: - # Get the selected value - model = combobox.get_model() - selected_image_set = model[tree_iter][0] - - # Store to the preferences - self.prefs.selected_image_set = selected_image_set - self.prefs.source_folder = os.path.abspath(os.path.join(PREFERENCES_URI, os.pardir)) + \ - "/5.4/images/included_image_sets/" + selected_image_set + "/" - - # Load all possible options to the comboboxes - image_names = self.images.get_images_from_folder(self.prefs.source_folder) - self.load_image_options_to_combo_boxes(image_names) - - # Image sets have the same names for the images: - # 9.jpg = Period 0 - # 1.jpg = Period 1 - # 2.jpg = Period 2 - # and so on.... - for i in range(0, 10): - self.ui.cb_periods[i].set_active(i + 1) - - - # +----------------------------------------------+ - # | Select a heic file to import | (None) 📄 | - # +----------------------------------------------+ - - def on_fc_heic_file_file_set(self, fc_button: Gtk.FileChooser): - """ User has a heic file selected with the FileChooserDialog - - Args: - fc_button (Gtk.FileChooser): Parameter about the selected file - """ - # The the absolute path to the heic file - file_path: str = fc_button.get_filename() - - # Extract the heic file - result = self.images.extract_heic_file(file_path) - - # Update the preferences - self.prefs.selected_image_set = "" - self.prefs.source_folder = PREFERENCES_URI + "/images/extracted_images/" - - # Load images only if the extraction was successfully - if result: - # Collect all extracted images and push them to the comboboxes - image_names = self.images.get_images_from_folder(self.prefs.source_folder) - self.load_image_options_to_combo_boxes(image_names) - else: - self.dialogs.message_dialog("Error during extraction") - - - # +------------------------------------------------------------+ - # | Select a source folder | 📂 Open file selection dialog | - # | /home/developer/Downloads/ - # +------------------------------------------------------------+ - - def on_btn_source_folder_clicked(self, button: Gtk.Button): - """ Button to choose an image source folder was clicked - - Args: - button (Gtk.Button): The clicked button - """ - folder = self.dialogs.source_folder_dialog() - files = self.images.get_images_from_folder(folder) - - # Update the preferences - self.prefs.selected_image_set = "" - self.prefs.source_folder = folder + "/" - - # Update the label - self.ui.lbl_source_folder.set_label(folder) - - # Update the image comboboxes - self.load_image_options_to_combo_boxes(files) - - # Load the values for the images from the preferences - for i in range(0, 10): - self.ui.cb_periods[i].set_active(0) - - if len(files) == 1: - self.dialogs.message_dialog("No image files found!") - - - def on_cb_period_preview_changed(self, combobox: Gtk.ComboBox): - """ User select an image from the ComboBox for the time period - - Args: - combobox (Gtk.ComboBox): The used ComboBox - """ - tree_iter = combobox.get_active_iter() - - combobox_name = Gtk.Buildable.get_name(combobox) - period_index = int(combobox_name[10:11]) - - if tree_iter is not None: - # Get the selected value - model = combobox.get_model() - image_file_name = model[tree_iter][0] - - # Store selection to preferences - self.prefs.period_images[period_index] = image_file_name - - # Build up image path - image_path = self.prefs.source_folder + image_file_name - - self.load_image_to_preview(self.ui.img_periods[period_index], image_path) - - - ## Location & Times - - def on_toggle_button_network_location_clicked(self, button: Gtk.ToggleButton): - """ User clicks on the ToggleButton for the network location - - Args: - button (Gtk.ToggleButton): Clicked ToggleButton - """ - if button.get_active(): - self.prefs.period_source = PeriodSourceEnum.NETWORKLOCATION - self.ui.tb_custom_location.set_active(False) - self.ui.tb_time_periods.set_active(False) - - self.ui.lbr_network_location.set_visible(True) - self.ui.lbr_current_location.set_visible(True) - self.ui.lbr_custom_location_longitude.set_visible(False) - self.ui.lbr_custom_location_latitude.set_visible(False) - self.ui.lbr_time_periods.set_visible(False) - - self.ui.spb_network_location_refresh_time.set_value(self.prefs.location_refresh_intervals) - - - # Display the location in the UI - current_location = self.location.get_location() - self.ui.lb_current_location.set_text("Latitude: " + current_location["latitude"] + \ - ", Longitude: " + current_location["longitude"]) - - # Store the location to the preferences - self.prefs.latitude_auto = float(current_location["latitude"]) - self.prefs.longitude_auto = float(current_location["longitude"]) - - self.refresh_chart() - - - def on_toggle_button_custom_location_clicked(self, button: Gtk.ToggleButton): - if button.get_active(): - self.prefs.period_source = PeriodSourceEnum.CUSTOMLOCATION - self.ui.tb_network_location.set_active(False) - self.ui.tb_time_periods.set_active(False) - - self.ui.lbr_network_location.set_visible(False) - self.ui.lbr_current_location.set_visible(False) - self.ui.lbr_custom_location_longitude.set_visible(True) - self.ui.lbr_custom_location_latitude.set_visible(True) - self.ui.lbr_time_periods.set_visible(False) - - self.ui.etr_latitude.set_text(str(self.prefs.latitude_custom)) - self.ui.etr_longitude.set_text(str(self.prefs.longitude_custom)) - - - def on_toggle_button_time_periods_clicked(self, button: Gtk.ToggleButton): - if button.get_active(): - self.prefs.period_source = PeriodSourceEnum.CUSTOMTIMEPERIODS - self.ui.tb_network_location.set_active(False) - self.ui.tb_custom_location.set_active(False) - - self.ui.lbr_network_location.set_visible(False) - self.ui.lbr_current_location.set_visible(False) - self.ui.lbr_custom_location_longitude.set_visible(False) - self.ui.lbr_custom_location_latitude.set_visible(False) - self.ui.lbr_time_periods.set_visible(True) - - - for i in range(0, 9): - pref_value = self.prefs.period_custom_start_time[i + 1] - time_parts = [int(pref_value[0:pref_value.find(":")]), int(pref_value[pref_value.find(":") + 1:])] - - self.ui.spb_periods_hour[i].set_value(time_parts[0]) - self.ui.spb_periods_minute[i].set_value(time_parts[1]) - - - - def on_spb_period_value_changed(self, spin_button: Gtk.SpinButton): - """ Callback if one of the time spinners (minute or hour) will be clicked - - (1) (2) (3) - Previous period Current period Next period - 12:34 - 14:40 14:41 - 16:20 16:21 - 17:30 - ^ - Variable to change - - Args: - spin_button (Gtk.SpinButton): SpinButton which was changed - """ - spin_button_name = Gtk.Buildable.get_name(spin_button) - index = int(spin_button_name[11:12]) - 1 - - # Determe time string and store to prefs - time_current_start = datetime(2024,1,1, int(self.ui.spb_periods_hour[index].get_value()), int(self.ui.spb_periods_minute[index].get_value())) - time_current_start_str = str(time_current_start.hour).rjust(2, '0') + ":" + str(time_current_start.minute).rjust(2, '0') - - self.prefs.period_custom_start_time[index + 1] = time_current_start_str - - - time_previous_end = time_current_start - timedelta(minutes=1) - self.ui.lb_period_end[index].set_text(str(time_previous_end.hour).rjust(2, '0') + ":" + str(time_previous_end.minute).rjust(2, '0')) - - - self.refresh_chart() - - - def on_spb_network_location_refresh_time_changed(self, spin_button: Gtk.SpinButton): - """ User changed the refresh time of network location estimation - - Args: - spin_button (Gtk.SpinButton): The used SpinButton - """ - self.prefs.location_refresh_intervals = spin_button.get_value() - - - def on_etr_longitude_changed(self, entry: Gtk.Entry): - """ User changes the value of the longitude Entry - - Args: - entry (Gtk.Entry): The manipulated Entry object - """ - try: - self.prefs.longitude_custom = float(entry.get_text()) - self.refresh_chart() - except: - pass - - - def on_etr_latitude_changed(self, entry: Gtk.Entry): - """ User changes the value of the latitude Entry - - Args: - entry (Gtk.Entry): The manipulated Entry object - """ - try: - self.prefs.latitude_custom = float(entry.get_text()) - self.refresh_chart() - except: - pass - - - # Behaviour - - def on_cb_picture_aspect_changed(self, combobox: Gtk.ComboBox): - tree_iter = combobox.get_active_iter() - - if tree_iter is not None: - model = combobox.get_model() - self.prefs.picture_aspect = model[tree_iter][0] - - def on_sw_dynamic_background_color_state_set(self, switch: Gtk.Switch, state): - self.prefs.dynamic_background_color = state - - - # About - - def on_cinnamon_spices_website_button_clicked(self, button: Gtk.Button): - """ Callback for the button to navigate to the Cinnamon Spices web page of this project - - Args: - button (Gtk.Button): Button which was clicked - """ - subprocess.Popen(["xdg-open", "https://cinnamon-spices.linuxmint.com/extensions/view/97"]) - - - def on_github_website_button_clicked(self, button: Gtk.Button): - """ Callback for the button to navigate to the GitHub web page of this project - - Args: - button (Gtk.Button): Button which was clicked - """ - subprocess.Popen(["xdg-open", "https://github.com/TobiZog/cinnamon-dynamic-wallpaper"]) - - - def on_create_issue_button_clicked(self, button): - """ Callback for the button to navigate to the Issues page on GitHub of this project - - Args: - button (Gtk.Button): Button which was clicked - """ - subprocess.Popen(["xdg-open", "https://github.com/TobiZog/cinnamon-dynamic-wallpaper/issues/new"]) - - - def on_ok(self, *args): - """ Callback for the OK button in the top bar - """ - try: - self.on_apply() - except: - pass - - # Close the window - self.on_destroy() - - - def on_apply(self, *args): - """ Callback for the Apply button in the top bar - """ - # Store all values to the JSON file - self.prefs.store_preferences() - - # Use the new settings - loop = Loop() - loop.exchange_image() - - -if __name__ == "__main__": - Preferences().show() \ No newline at end of file diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icons/icon.png b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/icons/icon.png similarity index 100% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icons/icon.png rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/icons/icon.png diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icons/icon.svg b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/icons/icon.svg similarity index 100% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/icons/icon.svg rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/icons/icon.svg diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/images/dynamic_background_example.png b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/res/images/dynamic_background_example.png new file mode 100644 index 0000000000000000000000000000000000000000..fac85f38a74c3cd484739e69ea9b060fdec89ea3 GIT binary patch literal 42789 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 True False - 00:00 - 07:35 + 00:00 - 00:59 @@ -531,7 +531,7 @@ True False - 08:15 - 08:43 + 02:00 - 02:59 @@ -545,7 +545,7 @@ True False - 11:13 - 13:12 + 04:00 - 04:59 @@ -559,7 +559,7 @@ True False - 14:42 - 15:41 + 06:00 - 06:59 @@ -573,7 +573,7 @@ True False - 16:13 - 16:52 + 08:00 - 08:59 @@ -789,7 +789,7 @@ True False - 07:35 - 08:14 + 01:00 - 01:59 @@ -803,7 +803,7 @@ True False - 08:44 - 11:12 + 03:00 - 03:59 @@ -817,7 +817,7 @@ True False - 13:13 - 14:41 + 05:00 - 05:59 @@ -831,7 +831,7 @@ True False - 15:41 - 16:12 + 07:00 - 07:59 @@ -845,7 +845,7 @@ True False - 16:53 - 23:59 + 09:00 - 09:59 @@ -1147,7 +1147,7 @@ none False - + True True @@ -1173,7 +1173,7 @@ - + True True adjustment1 @@ -1189,6 +1189,50 @@ + + + True + True + + + True + False + 8 + 8 + 8 + 8 + 8 + 8 + True + + + True + False + start + Location provider + + + False + True + 0 + + + + + True + False + + + + False + True + 1 + + + + + + True @@ -2427,6 +2471,23 @@ 8 8 vertical + 8 + + + True + False + start + Scaling + + + + + + False + True + 0 + + True @@ -2448,6 +2509,7 @@ 12 12 12 + True True @@ -2478,6 +2540,36 @@ + + + False + True + 3 + 1 + + + + + True + False + start + Dynamic Background color + + + + + + False + True + 2 + + + + + True + False + none + False True @@ -2498,7 +2590,7 @@ False start True - Dynamic Background color + Use dynamic Background color to create a gradient False @@ -2510,7 +2602,6 @@ True True - False @@ -2527,7 +2618,19 @@ False True 3 - 0 + 3 + + + + + True + False + images/dynamic_background_example.png + + + False + True + 4 diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/cinnamon_pref_handler.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/cinnamon_pref_handler.py deleted file mode 100644 index 37794dc7..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/cinnamon_pref_handler.py +++ /dev/null @@ -1,100 +0,0 @@ -import os, json - -class Cinnamon_Pref_Handler: - """ Class to work with the Cinnamon Extension preference format - """ - def __init__(self) -> None: - # Location of the Cinnamon preference file since Cinnamon 5.4 - self.pref_location = os.path.expanduser("~") + \ - "/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json" - - self.load_preferences() - - - def load_preferences(self): - """ Load the JSON preferences to the Preference object - """ - with open(self.pref_location, "r") as pref_file: - pref_data = json.load(pref_file) - - self.picture_aspect = pref_data['picture_aspect']['value'] - self.dynamic_background_color = pref_data['dynamic_background_color']['value'] - self.image_source = pref_data['image_source']['value'] - self.selected_image_set = pref_data['selected_image_set']['value'] - self.source_folder = pref_data['source_folder']['value'] - - self.period_images = [ - pref_data['period_0_image']['value'], - pref_data['period_1_image']['value'], - pref_data['period_2_image']['value'], - pref_data['period_3_image']['value'], - pref_data['period_4_image']['value'], - pref_data['period_5_image']['value'], - pref_data['period_6_image']['value'], - pref_data['period_7_image']['value'], - pref_data['period_8_image']['value'], - pref_data['period_9_image']['value'] - ] - - self.period_source = pref_data['period_source']['value'] - self.location_refresh_intervals = pref_data['location_refresh_intervals']['value'] - self.latitude_auto = pref_data['latitude_auto']['value'] - self.longitude_auto = pref_data['longitude_auto']['value'] - self.latitude_custom = pref_data['latitude_custom']['value'] - self.longitude_custom = pref_data['longitude_custom']['value'] - - self.period_custom_start_time = [ - pref_data['period_0_custom_start_time']['value'], - pref_data['period_1_custom_start_time']['value'], - pref_data['period_2_custom_start_time']['value'], - pref_data['period_3_custom_start_time']['value'], - pref_data['period_4_custom_start_time']['value'], - pref_data['period_5_custom_start_time']['value'], - pref_data['period_6_custom_start_time']['value'], - pref_data['period_7_custom_start_time']['value'], - pref_data['period_8_custom_start_time']['value'], - pref_data['period_9_custom_start_time']['value'] - ] - - - def store_preferences(self): - """ Store the values of the Preference object to the JSON file - """ - with open(self.pref_location, "r") as pref_file: - pref_data = json.load(pref_file) - - pref_data['picture_aspect']['value'] = self.picture_aspect - pref_data['dynamic_background_color']['value'] = self.dynamic_background_color - pref_data['image_source']['value'] = self.image_source - pref_data['selected_image_set']['value'] = self.selected_image_set - pref_data['source_folder']['value'] = self.source_folder - pref_data['period_0_image']['value'] = self.period_images[0] - pref_data['period_1_image']['value'] = self.period_images[1] - pref_data['period_2_image']['value'] = self.period_images[2] - pref_data['period_3_image']['value'] = self.period_images[3] - pref_data['period_4_image']['value'] = self.period_images[4] - pref_data['period_5_image']['value'] = self.period_images[5] - pref_data['period_6_image']['value'] = self.period_images[6] - pref_data['period_7_image']['value'] = self.period_images[7] - pref_data['period_8_image']['value'] = self.period_images[8] - pref_data['period_9_image']['value'] = self.period_images[9] - pref_data['period_source']['value'] = self.period_source - pref_data['location_refresh_intervals']['value'] = self.location_refresh_intervals - pref_data['latitude_auto']['value'] = self.latitude_auto - pref_data['longitude_auto']['value'] = self.longitude_auto - pref_data['latitude_custom']['value'] = self.latitude_custom - pref_data['longitude_custom']['value'] = self.longitude_custom - pref_data['period_0_custom_start_time']['value'] = self.period_custom_start_time[0] - pref_data['period_1_custom_start_time']['value'] = self.period_custom_start_time[1] - pref_data['period_2_custom_start_time']['value'] = self.period_custom_start_time[2] - pref_data['period_3_custom_start_time']['value'] = self.period_custom_start_time[3] - pref_data['period_4_custom_start_time']['value'] = self.period_custom_start_time[4] - pref_data['period_5_custom_start_time']['value'] = self.period_custom_start_time[5] - pref_data['period_6_custom_start_time']['value'] = self.period_custom_start_time[6] - pref_data['period_7_custom_start_time']['value'] = self.period_custom_start_time[7] - pref_data['period_8_custom_start_time']['value'] = self.period_custom_start_time[8] - pref_data['period_9_custom_start_time']['value'] = self.period_custom_start_time[9] - - # Write to file - with open(self.pref_location, "w") as pref_file: - json.dump(pref_data, pref_file, separators=(',', ':'), indent=4) diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/display.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/display.py deleted file mode 100644 index ddd22748..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/display.py +++ /dev/null @@ -1,20 +0,0 @@ -import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gdk - -class Display: - """ Handling display informations and actions - """ - def __init__(self) -> None: - self.display = Gdk.Display.get_default() - - - def get_screen_height(self) -> int: - """ Estimate the height resolution of the primary display - - Returns: - int: Height in pixel - """ - geometry = self.display.get_monitor(0).get_geometry() - - return geometry.height diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/location.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/location.py deleted file mode 100644 index bc8169fc..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/location.py +++ /dev/null @@ -1,22 +0,0 @@ -import urllib.request, json - -class Location(): - """ Class to handle location requests - """ - def __init__(self): - self.GEO_URL = "https://get.geojs.io/v1/ip/geo.json" - - def get_location(self) -> dict: - """ Request the location via network - - Returns: - dict: latitude and longitude - """ - request = urllib.request.urlopen(self.GEO_URL) - - data = json.load(request) - - return { - "latitude": data["latitude"], - "longitude": data["longitude"] - } diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/ui.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/ui.py deleted file mode 100644 index daa0a695..00000000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/ui.py +++ /dev/null @@ -1,147 +0,0 @@ -import gi -gi.require_version("Gtk", "3.0") -from gi.repository import Gtk - -class UI: - """ Class to handle UI components and actions - """ - def __init__(self, builder: Gtk.Builder) -> None: - # Page 1 - self.tb_image_set: Gtk.ToggleButton = builder.get_object("tb_image_set") - self.tb_heic_file: Gtk.ToggleButton = builder.get_object("tb_heic_file") - self.tb_source_folder: Gtk.ToggleButton = builder.get_object("tb_source_folder") - self.img_tb_image_set: Gtk.Image = builder.get_object("img_tb_image_set") - self.img_tb_heic_file: Gtk.Image = builder.get_object("img_tb_heic_file") - self.img_tb_source_folder: Gtk.Image = builder.get_object("img_tb_source_folder") - - # Image set - self.lbr_image_set: Gtk.ListBoxRow = builder.get_object("lbr_image_set") - self.cb_image_set: Gtk.ComboBox = builder.get_object("cb_image_set") - - # HEIC file - self.lbr_heic_file: Gtk.ListBoxRow = builder.get_object("lbr_heic_file") - - # Source folder - self.lbr_source_folder: Gtk.ListBoxRow = builder.get_object("lbr_source_folder") - self.btn_source_folder: Gtk.Button = builder.get_object("btn_source_folder") - self.lbl_source_folder: Gtk.Label = builder.get_object("lbl_source_folder") - - # Time bar chart - self.img_bar_images: Gtk.Image = builder.get_object("img_bar_images") - self.etr_periods: list[Gtk.Entry] = [ - builder.get_object("etr_period_1"), builder.get_object("etr_period_2"), - builder.get_object("etr_period_3"), builder.get_object("etr_period_4"), - builder.get_object("etr_period_5"), builder.get_object("etr_period_6"), - builder.get_object("etr_period_7"), builder.get_object("etr_period_8"), - builder.get_object("etr_period_9"), builder.get_object("etr_period_10"), - ] - - self.img_periods: list[Gtk.Image] = [ - builder.get_object("img_period_0"), builder.get_object("img_period_1"), - builder.get_object("img_period_2"), builder.get_object("img_period_3"), - builder.get_object("img_period_4"), builder.get_object("img_period_5"), - builder.get_object("img_period_6"), builder.get_object("img_period_7"), - builder.get_object("img_period_8"), builder.get_object("img_period_9"), - ] - - self.cb_periods: list[Gtk.ComboBox] = [ - builder.get_object("cb_period_0"), builder.get_object("cb_period_1"), - builder.get_object("cb_period_2"), builder.get_object("cb_period_3"), - builder.get_object("cb_period_4"), builder.get_object("cb_period_5"), - builder.get_object("cb_period_6"), builder.get_object("cb_period_7"), - builder.get_object("cb_period_8"), builder.get_object("cb_period_9"), - ] - - - - #### Page 2: Location & Times - self.tb_network_location: Gtk.ToggleButton = builder.get_object("tb_network_location") - self.tb_custom_location: Gtk.ToggleButton = builder.get_object("tb_custom_location") - self.tb_time_periods: Gtk.ToggleButton = builder.get_object("tb_time_periods") - self.img_tb_network_location: Gtk.Image = builder.get_object("img_tb_network_location") - self.img_tb_custom_location: Gtk.Image = builder.get_object("img_tb_custom_location") - self.img_tb_time_periods: Gtk.Image = builder.get_object("img_tb_time_periods") - - # Network Location - self.lb_current_location: Gtk.Label = builder.get_object("lb_current_location") - self.lbr_current_location: Gtk.ListBoxRow = builder.get_object("lbr_current_location") - self.lbr_network_location: Gtk.ListBoxRow = builder.get_object("lbr_network_location") - self.spb_network_location_refresh_time: Gtk.SpinButton = builder.get_object("spb_network_location_refresh_time") - - # Custom location - self.lbr_custom_location_longitude: Gtk.ListBoxRow = builder.get_object("lbr_custom_location_longitude") - self.lbr_custom_location_latitude: Gtk.ListBoxRow = builder.get_object("lbr_custom_location_latitude") - self.lbr_time_periods: Gtk.ListBoxRow = builder.get_object("lbr_time_periods") - self.etr_longitude: Gtk.Entry = builder.get_object("etr_longitude") - self.etr_latitude: Gtk.Entry = builder.get_object("etr_latitude") - self.img_bar_times: Gtk.Image = builder.get_object("img_bar_times") - self.spb_periods_hour: list[Gtk.SpinButton] = [ - builder.get_object("spb_period_1_hour"), - builder.get_object("spb_period_2_hour"), - builder.get_object("spb_period_3_hour"), - builder.get_object("spb_period_4_hour"), - builder.get_object("spb_period_5_hour"), - builder.get_object("spb_period_6_hour"), - builder.get_object("spb_period_7_hour"), - builder.get_object("spb_period_8_hour"), - builder.get_object("spb_period_9_hour"), - ] - self.spb_periods_minute: list[Gtk.SpinButton] = [ - builder.get_object("spb_period_1_minute"), - builder.get_object("spb_period_2_minute"), - builder.get_object("spb_period_3_minute"), - builder.get_object("spb_period_4_minute"), - builder.get_object("spb_period_5_minute"), - builder.get_object("spb_period_6_minute"), - builder.get_object("spb_period_7_minute"), - builder.get_object("spb_period_8_minute"), - builder.get_object("spb_period_9_minute") - ] - self.lb_period_end: list[Gtk.Label] = [ - builder.get_object("lb_period_0_end"), builder.get_object("lb_period_1_end"), - builder.get_object("lb_period_2_end"), builder.get_object("lb_period_3_end"), - builder.get_object("lb_period_4_end"), builder.get_object("lb_period_5_end"), - builder.get_object("lb_period_6_end"), builder.get_object("lb_period_7_end"), - builder.get_object("lb_period_8_end"), builder.get_object("lb_period_9_end"), - ] - - - # Page 3: Behaviour - self.cb_picture_aspect: Gtk.ComboBox = builder.get_object("cb_picture_aspect") - self.sw_dynamic_background_color: Gtk.Switch = builder.get_object("sw_dynamic_background_color") - - - def set_active_combobox_item(self, combobox: Gtk.ComboBoxText, active_item: str): - """ Change active item in combobox by String value - - Args: - combobox (Gtk.ComboBoxText): ComboBox to set active - active_item (str): String item to set active - """ - list_store = combobox.get_model() - - for i in range(0, len(list_store)): - row = list_store[i] - if row[0] == active_item: - combobox.set_active(i) - - - def add_items_to_combo_box(self, combobox: Gtk.ComboBox, items: list): - """ Add items to a combo box - - Args: - combobox (Gtk.ComboBox): ComboBox where to add the options - items (list): Possible options - """ - model = combobox.get_model() - store = Gtk.ListStore(str) - - for image_set in items: - store.append([image_set]) - - combobox.set_model(store) - - if model == None: - renderer_text = Gtk.CellRendererText() - combobox.pack_start(renderer_text, True) - combobox.add_attribute(renderer_text, "text", 0) diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/settings-schema.json b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/settings-schema.json index 94b2cf08..61fa3b5c 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/settings-schema.json +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/settings-schema.json @@ -71,6 +71,10 @@ "type": "generic", "default": 15 }, + "network_location_provider": { + "type": "generic", + "default": "geojs.io" + }, "latitude_auto": { "type": "generic", "default": 0 diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/enums/ImageSourceEnum.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/ImageSourceEnum.py similarity index 100% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/enums/ImageSourceEnum.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/ImageSourceEnum.py diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/NetworkLocationProvider.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/NetworkLocationProvider.py new file mode 100644 index 00000000..e1de45ce --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/NetworkLocationProvider.py @@ -0,0 +1,4 @@ +class NetworkLocationProvider(enumerate): + GEOJS = "geojs.io" + IPAPI = "ip-api.com" + IPWHOIS = "ipwho.is" diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/enums/PeriodSourceEnum.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/PeriodSourceEnum.py similarity index 100% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/enums/PeriodSourceEnum.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/enums/PeriodSourceEnum.py diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/main.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/main.py new file mode 100755 index 00000000..518ba702 --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/main.py @@ -0,0 +1,17 @@ +#!/usr/bin/python3 + +import sys, datetime +from view.main_window import * +from model.main_view_model import * + +if __name__ == "__main__": + if len(sys.argv) == 1: + # Load the configuration window + main = Main_Window() + main.show() + + elif sys.argv[1] == "loop": + # Run the methods which updates the data + view_model = Main_View_Model() + view_model.refresh_image() + view_model.set_background_gradient() diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/model/main_view_model.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/model/main_view_model.py new file mode 100644 index 00000000..69a1aa69 --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/model/main_view_model.py @@ -0,0 +1,259 @@ +import os, time +from PIL import Image +from gi.repository import Gio, Gdk + +from service.cinnamon_pref_handler import * +from service.suntimes import * +from service.time_bar_chart import * +from service.location import * +from enums.PeriodSourceEnum import * + +class Main_View_Model: + """ The main ViewModel for the application + """ + + def __init__(self) -> None: + """ Initialization + """ + # Paths + self.WORKING_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + self.RES_DIR = self.WORKING_DIR + "/res" + self.IMAGES_DIR = self.RES_DIR + "/images" + self.GLADE_URI = self.RES_DIR + "/preferences.glade" + self.TIMEBAR_URI = self.WORKING_DIR + "/src/time_bar.svg" + self.TIMEBAR_URI_POLYLINES = self.WORKING_DIR + "/src/time_bar_polylines.svg" + self.PREF_URI = os.path.expanduser("~") + \ + "/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json" + + # Datasets + self.image_sets = ["aurora", "beach", "bitday", "cliffs", "earth", "gradient", "lakeside", "mountains", "sahara"] + self.picture_aspects = ["centered", "scaled", "stretched", "zoom", "spanned"] + self.network_location_provider = ["geojs.io", "ip-api.com", "ipwho.is"] + + # Objects from scripts + self.cinnamon_prefs = Cinnamon_Pref_Handler() + self.time_bar_chart = Time_Bar_Chart() + self.suntimes = Suntimes() + self.location = Location() + + self.background_settings = Gio.Settings.new("org.cinnamon.desktop.background") + + # Other Variables + self.display = Gdk.Display.get_default() + self.screen_height = self.display.get_monitor(0).get_geometry().height + self.breakpoint_ui = 1000 + + + def refresh_charts(self): + """ Refreshes the two variants of the time bar charts + """ + # Stores the start times of the periods in minutes since midnight + time_periods_min = [] + + if self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: + for i in range(0, 10): + time_str = self.cinnamon_prefs.period_custom_start_time[i] + + time_periods_min.append(int(time_str[0:2]) * 60 + int(time_str[3:5])) + else: + if self.cinnamon_prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: + self.suntimes.calc_suntimes(float(self.cinnamon_prefs.latitude_auto), float(self.cinnamon_prefs.longitude_auto)) + else: + self.suntimes.calc_suntimes(float(self.cinnamon_prefs.latitude_custom), float(self.cinnamon_prefs.longitude_custom)) + + + # Get all time periods. Store the minutes to the list and print the values to the text views + for i in range(0, 10): + time_range_now = self.suntimes.day_periods[i] + time_periods_min.append(time_range_now.hour * 60 + time_range_now.minute) + + + # Create time bar + # Reduce size for small displays + if self.screen_height < self.breakpoint_ui: + bar_width = 1150 + bar_height = 110 + else: + bar_width = 1300 + bar_height = 150 + + self.time_bar_chart.create_bar_chart_with_polylines(self.TIMEBAR_URI_POLYLINES, bar_width, bar_height, time_periods_min) + self.time_bar_chart.create_bar_chart(self.TIMEBAR_URI, bar_width, bar_height, time_periods_min) + + + def refresh_location(self) -> bool: + """ Updating the location by IP, store the result to cinnamon_prefs + Run it in a parallel thread to avoid UI freeze! + + Returns: + bool: Successful or not + """ + current_location = self.location.get_location(self.cinnamon_prefs.network_location_provider) + + if current_location['success']: + self.cinnamon_prefs.latitude_auto = current_location['latitude'] + self.cinnamon_prefs.longitude_auto = current_location['longitude'] + + return current_location['success'] + + + def string_to_time_converter(self, raw_str: str) -> time: + """ Convert a time string like "12:34" to a time object + + Args: + raw_str (str): Raw string + + Returns: + time: Time object + """ + hour = raw_str[0:raw_str.find(":")] + minute = raw_str[raw_str.find(":") + 1:] + + time(1, 2) + + return time(hour=int(hour), minute=int(minute)) + + + def time_to_string_converter(self, _time: time) -> str: + """ Convert a time object to a string like "12:34" + + Args: + time (time): Given time object to convert + + Returns: + str: Converted string + """ + return "{:0>2}:{:0>2}".format(_time.hour, _time.minute) + + + def calulate_time_periods(self) -> list[time]: + """ Calculate the ten time periods based on the period source in the preferences + + Returns: + list[time]: Time periods + """ + result = [] + + if self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMTIMEPERIODS: + # User uses custom time periods + for i in range(0, 10): + result.append(self.string_to_time_converter(self.cinnamon_prefs.period_custom_start_time[i])) + else: + # Time periods have to be estimate by coordinates + if self.cinnamon_prefs.period_source == PeriodSourceEnum.NETWORKLOCATION: + # Get coordinates from the network + self.suntimes.calc_suntimes(self.cinnamon_prefs.latitude_auto, self.cinnamon_prefs.longitude_auto) + + elif self.cinnamon_prefs.period_source == PeriodSourceEnum.CUSTOMLOCATION: + # Get coordinates from user input + self.suntimes.calc_suntimes(self.cinnamon_prefs.latitude_custom, self.cinnamon_prefs.longitude_custom) + + # Return the time periods + result = self.suntimes.day_periods + + return result + + + def refresh_image(self): + """ Replace the desktop image if needed + """ + start_times = self.calulate_time_periods() + + # Get the time of day + time_now = time(datetime.now().hour, datetime.now().minute) + + # Assign the last image as fallback + self.current_image_uri = self.cinnamon_prefs.source_folder + self.cinnamon_prefs.period_images[9] + + for i in range(0, 9): + # Replace the image URI, if it's not the last time period of the day + if start_times[i] <= time_now and time_now < start_times[i + 1]: + self.current_image_uri = self.cinnamon_prefs.source_folder + self.cinnamon_prefs.period_images[i] + break + + # Set the background + self.background_settings['picture-uri'] = "file://" + self.current_image_uri + + # Set background stretching + self.background_settings['picture-options'] = self.cinnamon_prefs.picture_aspect + + + def get_images_from_folder(self, URI: str) -> list: + """ List all images in a folder + + Args: + URI (str): Absolute path of the folder + + Returns: + list: List of file names which are images + """ + items = [] + + for file in os.listdir(URI): + if file.endswith(("jpg", "jpeg", "png", "bmp", "svg")): + items.append(file) + + items.sort() + return items + + + def extract_heic_file(self, file_uri: str) -> bool: + """ Extract a heic file to an internal folder + + Args: + file_uri (str): Absolute path to the heic file + + Returns: + bool: Extraction was successful + """ + try: + extract_folder = self.IMAGES_DIR + "/extracted_images/" + + file_name: str = file_uri[file_uri.rfind("/") + 1:] + file_name = file_name[:file_name.rfind(".")] + + # Create the buffer folder if its not existing + try: + os.mkdir(extract_folder) + except: + pass + + # Cleanup the folder + for file in self.get_images_from_folder(extract_folder): + os.remove(extract_folder + file) + + # Extract the HEIC file + os.system("heif-convert '" + file_uri + "' '" + extract_folder + file_name + ".jpg'") + + return True + except: + return False + + + def set_background_gradient(self): + """ Setting a gradient background to hide images, which are not high enough + """ + # Load the image + try: + im = Image.open(self.current_image_uri) + pix = im.load() + + # Width and height of the current setted image + width, height = im.size + + # Color of the top and bottom pixel in the middle of the image + top_color = pix[width / 2,0] + bottom_color = pix[width / 2, height - 1] + + # Create the gradient + self.background_settings['color-shading-type'] = "vertical" + + if self.cinnamon_prefs.dynamic_background_color: + self.background_settings['primary-color'] = f"#{top_color[0]:x}{top_color[1]:x}{top_color[2]:x}" + self.background_settings['secondary-color'] = f"#{bottom_color[0]:x}{bottom_color[1]:x}{bottom_color[2]:x}" + else: + self.background_settings['primary-color'] = "#000000" + self.background_settings['secondary-color'] = "#000000" + except: + self.background_settings['primary-color'] = "#000000" + self.background_settings['secondary-color'] = "#000000" diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/cinnamon_pref_handler.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/cinnamon_pref_handler.py new file mode 100644 index 00000000..5162ec39 --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/cinnamon_pref_handler.py @@ -0,0 +1,132 @@ +import os, json + +class Cinnamon_Pref_Handler: + """ Class to work with the Cinnamon Extension preference format + """ + def __init__(self) -> None: + # Location of the Cinnamon preference file since Cinnamon 5.4 + self.pref_location = os.path.expanduser("~") + \ + "/.config/cinnamon/spices/cinnamon-dynamic-wallpaper@TobiZog/cinnamon-dynamic-wallpaper@TobiZog.json" + + self.load_preferences() + + + def extract_json(self, parameter: str) -> str: + """ Get a parameter from the json dictionary safely + + Args: + parameter (str): Parameter to request + + Returns: + str: Value of the parameter (or "" if not existing) + """ + try: + return self.pref_data[parameter]['value'] + except: + return "" + + + def load_preferences(self): + """ Load the JSON preferences to the Preference object + """ + with open(self.pref_location, "r") as pref_file: + self.pref_data = json.load(pref_file) + + self.picture_aspect = self.extract_json('picture_aspect') + self.dynamic_background_color = self.extract_json('dynamic_background_color') + self.image_source = self.extract_json('image_source') + self.selected_image_set = self.extract_json('selected_image_set') + self.source_folder = self.extract_json('source_folder') + + self.period_images = [ + self.extract_json('period_0_image'), + self.extract_json('period_1_image'), + self.extract_json('period_2_image'), + self.extract_json('period_3_image'), + self.extract_json('period_4_image'), + self.extract_json('period_5_image'), + self.extract_json('period_6_image'), + self.extract_json('period_7_image'), + self.extract_json('period_8_image'), + self.extract_json('period_9_image') + ] + + self.period_source =self.extract_json('period_source') + self.location_refresh_intervals =self.extract_json('location_refresh_intervals') + self.network_location_provider =self.extract_json('network_location_provider') + self.latitude_auto =self.extract_json('latitude_auto') + self.longitude_auto =self.extract_json('longitude_auto') + self.latitude_custom =self.extract_json('latitude_custom') + self.longitude_custom =self.extract_json('longitude_custom') + + self.period_custom_start_time = [ + self.extract_json('period_0_custom_start_time'), + self.extract_json('period_1_custom_start_time'), + self.extract_json('period_2_custom_start_time'), + self.extract_json('period_3_custom_start_time'), + self.extract_json('period_4_custom_start_time'), + self.extract_json('period_5_custom_start_time'), + self.extract_json('period_6_custom_start_time'), + self.extract_json('period_7_custom_start_time'), + self.extract_json('period_8_custom_start_time'), + self.extract_json('period_9_custom_start_time') + ] + + + def value_to_json(self, parameter: str, value: str): + """ Storing safely a value to the dictionary + + Args: + parameter (str): Parameter to write + value (str): Value to write + """ + try: + self.pref_data[parameter]['value'] = value + except: + self.pref_data[parameter] = { + 'type': 'generic', + 'value': value + } + print(self.pref_data) + + + def store_preferences(self): + """ Store the values of the Preference object to the JSON file + """ + self.value_to_json('picture_aspect', self.picture_aspect) + self.value_to_json('dynamic_background_color', self.dynamic_background_color) + self.value_to_json('image_source', self.image_source) + self.value_to_json('selected_image_set', self.selected_image_set) + self.value_to_json('source_folder', self.source_folder) + self.value_to_json('period_0_image', self.period_images[0]) + self.value_to_json('period_1_image', self.period_images[1]) + self.value_to_json('period_2_image', self.period_images[2]) + self.value_to_json('period_3_image', self.period_images[3]) + self.value_to_json('period_4_image', self.period_images[4]) + self.value_to_json('period_5_image', self.period_images[5]) + self.value_to_json('period_6_image', self.period_images[6]) + self.value_to_json('period_7_image', self.period_images[7]) + self.value_to_json('period_8_image', self.period_images[8]) + self.value_to_json('period_9_image', self.period_images[9]) + self.value_to_json('period_source', self.period_source) + self.value_to_json('location_refresh_intervals', self.location_refresh_intervals) + self.value_to_json('network_location_provider', self.network_location_provider) + self.value_to_json('latitude_auto', self.latitude_auto) + self.value_to_json('longitude_auto', self.longitude_auto) + self.value_to_json('latitude_custom', self.latitude_custom) + self.value_to_json('longitude_custom', self.longitude_custom) + self.value_to_json('period_0_custom_start_time', self.period_custom_start_time[0]) + self.value_to_json('period_1_custom_start_time', self.period_custom_start_time[1]) + self.value_to_json('period_2_custom_start_time', self.period_custom_start_time[2]) + self.value_to_json('period_3_custom_start_time', self.period_custom_start_time[3]) + self.value_to_json('period_4_custom_start_time', self.period_custom_start_time[4]) + self.value_to_json('period_5_custom_start_time', self.period_custom_start_time[5]) + self.value_to_json('period_6_custom_start_time', self.period_custom_start_time[6]) + self.value_to_json('period_7_custom_start_time', self.period_custom_start_time[7]) + self.value_to_json('period_8_custom_start_time', self.period_custom_start_time[8]) + self.value_to_json('period_9_custom_start_time', self.period_custom_start_time[9]) + + + # Write to file + with open(self.pref_location, "w") as pref_file: + json.dump(self.pref_data, pref_file, separators=(',', ':'), indent=4) diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/images.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/images.py similarity index 100% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/images.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/images.py diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/location.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/location.py new file mode 100644 index 00000000..40831a1c --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/location.py @@ -0,0 +1,39 @@ +import urllib.request, json +from enums.NetworkLocationProvider import NetworkLocationProvider + +class Location(): + """ Class to handle location requests + """ + def get_location(self, provider: NetworkLocationProvider) -> dict: + """ Request the location via network + + Returns: + dict: latitude and longitude + """ + if provider == NetworkLocationProvider.GEOJS: + url = "http://get.geojs.io/v1/ip/geo.json" + elif provider == NetworkLocationProvider.IPAPI: + url = "http://ip-api.com/json/?fields=61439" + elif provider == NetworkLocationProvider.IPWHOIS: + url = "http://ipwho.is" + + try: + request = urllib.request.urlopen(url) + data = json.load(request) + + if provider == NetworkLocationProvider.GEOJS or provider == NetworkLocationProvider.IPWHOIS: + param_lat = "latitude" + param_lon = "longitude" + else: + param_lat = "lat" + param_lon = "lon" + + return { + "latitude": float(data[param_lat]), + "longitude": float(data[param_lon]), + "success": True + } + except: + return { + "success": False + } diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/suntimes.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/suntimes.py similarity index 97% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/suntimes.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/suntimes.py index 02e8fdec..65e6200e 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/suntimes.py +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/suntimes.py @@ -10,10 +10,6 @@ class Suntimes: """ def __init__(self) -> None: """ Initialization - - Args: - latitude (float): Latitude of the position - longitude (float): Longitude of the position """ self.today = datetime.now() diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/time_bar_chart.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/time_bar_chart.py similarity index 97% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/time_bar_chart.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/time_bar_chart.py index 26b16e10..70b2c751 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/time_bar_chart.py +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/service/time_bar_chart.py @@ -39,7 +39,7 @@ def create_bar_chart_with_polylines(self, save_location: str, image_width: int, self.image_code.insert(0, '' % (image_width, image_height)) self.image_code.append('') - file = open(save_location + "/time_bar_polylines.svg", "w") + file = open(save_location, "w") for i in self.image_code: file.write(i + '\n') @@ -63,7 +63,7 @@ def create_bar_chart(self, save_location: str, image_width: int, image_height: i self.image_code.insert(0, '' % (image_width, image_height)) self.image_code.append('') - file = open(save_location + "/time_bar.svg", "w") + file = open(save_location, "w") for i in self.image_code: file.write(i + '\n') diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/dialogs.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/dialogs.py similarity index 88% rename from cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/dialogs.py rename to cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/dialogs.py index 75c020bd..636c3e0a 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/scripts/dialogs.py +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/dialogs.py @@ -32,18 +32,17 @@ def source_folder_dialog(self) -> str: dialog.set_default_size(800, 400) response = dialog.run() + location = "" if response == Gtk.ResponseType.OK: location = dialog.get_filename() - elif response == Gtk.ResponseType.CANCEL: - location = "" dialog.destroy() return location - def message_dialog(self, message: str): + def message_dialog(self, message: str, type: Gtk.MessageType = Gtk.MessageType.INFO): """ Displaying a Gtk Message dialog to the user Args: @@ -52,7 +51,7 @@ def message_dialog(self, message: str): dialog = Gtk.MessageDialog( transient_for=self, flags=0, - message_type=Gtk.MessageType.INFO, + message_type=type, buttons=Gtk.ButtonsType.OK, text=message ) diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/main_window.py b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/main_window.py new file mode 100644 index 00000000..14e849d5 --- /dev/null +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/5.4/src/view/main_window.py @@ -0,0 +1,754 @@ +############################################################ +# Imports # +############################################################ + +# GTK +import gi +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, GdkPixbuf + +# Packages +import subprocess, threading, time +from datetime import timedelta, datetime, date + +# Local scripts +from model.main_view_model import * +from view.dialogs import * +from enums.ImageSourceEnum import * +from enums.PeriodSourceEnum import * + + +class Main_Window: + ############################################################ + # Lifecycle # + ############################################################ + + def __init__(self) -> None: + """ Initialize all UI components which should be handleable + + Args: + builder (Gtk.Builder): Gtk self.builder resource + """ + # View Model + self.view_model = Main_View_Model() + + # Glade + self.builder = Gtk.Builder() + self.builder.add_from_file(self.view_model.GLADE_URI) + self.builder.connect_signals(self) + + + # Objects from scripts + self.dialogs = Dialogs() + + + + # Page 1: Image Configuration + # Toggle Buttons + self.tb_image_set: Gtk.ToggleButton = self.builder.get_object("tb_image_set") + self.tb_heic_file: Gtk.ToggleButton = self.builder.get_object("tb_heic_file") + self.tb_source_folder: Gtk.ToggleButton = self.builder.get_object("tb_source_folder") + self.img_tb_image_set: Gtk.Image = self.builder.get_object("img_tb_image_set") + self.img_tb_heic_file: Gtk.Image = self.builder.get_object("img_tb_heic_file") + self.img_tb_source_folder: Gtk.Image = self.builder.get_object("img_tb_source_folder") + + # Image set + self.lbr_image_set: Gtk.ListBoxRow = self.builder.get_object("lbr_image_set") + self.cb_image_set: Gtk.ComboBox = self.builder.get_object("cb_image_set") + + # HEIC file + self.lbr_heic_file: Gtk.ListBoxRow = self.builder.get_object("lbr_heic_file") + + # Source folder + self.lbr_source_folder: Gtk.ListBoxRow = self.builder.get_object("lbr_source_folder") + self.btn_source_folder: Gtk.Button = self.builder.get_object("btn_source_folder") + self.lbl_source_folder: Gtk.Label = self.builder.get_object("lbl_source_folder") + + # Time bar chart + self.img_bar_images: Gtk.Image = self.builder.get_object("img_bar_images") + self.etr_periods: list[Gtk.Label] = [ + self.builder.get_object("etr_period_1"), self.builder.get_object("etr_period_2"), + self.builder.get_object("etr_period_3"), self.builder.get_object("etr_period_4"), + self.builder.get_object("etr_period_5"), self.builder.get_object("etr_period_6"), + self.builder.get_object("etr_period_7"), self.builder.get_object("etr_period_8"), + self.builder.get_object("etr_period_9"), self.builder.get_object("etr_period_10"), + ] + + self.img_periods: list[Gtk.Image] = [ + self.builder.get_object("img_period_0"), self.builder.get_object("img_period_1"), + self.builder.get_object("img_period_2"), self.builder.get_object("img_period_3"), + self.builder.get_object("img_period_4"), self.builder.get_object("img_period_5"), + self.builder.get_object("img_period_6"), self.builder.get_object("img_period_7"), + self.builder.get_object("img_period_8"), self.builder.get_object("img_period_9"), + ] + + self.cb_periods: list[Gtk.ComboBox] = [ + self.builder.get_object("cb_period_0"), self.builder.get_object("cb_period_1"), + self.builder.get_object("cb_period_2"), self.builder.get_object("cb_period_3"), + self.builder.get_object("cb_period_4"), self.builder.get_object("cb_period_5"), + self.builder.get_object("cb_period_6"), self.builder.get_object("cb_period_7"), + self.builder.get_object("cb_period_8"), self.builder.get_object("cb_period_9"), + ] + + + #### Page 2: Location & Times + # Toggle Buttons + self.tb_network_location: Gtk.ToggleButton = self.builder.get_object("tb_network_location") + self.tb_custom_location: Gtk.ToggleButton = self.builder.get_object("tb_custom_location") + self.tb_time_periods: Gtk.ToggleButton = self.builder.get_object("tb_time_periods") + self.img_tb_network_location: Gtk.Image = self.builder.get_object("img_tb_network_location") + self.img_tb_custom_location: Gtk.Image = self.builder.get_object("img_tb_custom_location") + self.img_tb_time_periods: Gtk.Image = self.builder.get_object("img_tb_time_periods") + + # Network Location + self.lbr_network_refresh_time: Gtk.ListBoxRow = self.builder.get_object("lbr_network_refresh_time") + self.spb_network_refresh_time: Gtk.SpinButton = self.builder.get_object("spb_network_refresh_time") + self.lbr_network_provider: Gtk.ListBoxRow = self.builder.get_object("lbr_network_provider") + self.cb_network_provider: Gtk.ComboBox = self.builder.get_object("cb_network_provider") + self.lbr_current_location: Gtk.ListBoxRow = self.builder.get_object("lbr_current_location") + self.lb_current_location: Gtk.Label = self.builder.get_object("lb_current_location") + + # Custom location + self.lbr_custom_location_longitude: Gtk.ListBoxRow = self.builder.get_object("lbr_custom_location_longitude") + self.lbr_custom_location_latitude: Gtk.ListBoxRow = self.builder.get_object("lbr_custom_location_latitude") + self.lbr_time_periods: Gtk.ListBoxRow = self.builder.get_object("lbr_time_periods") + self.etr_longitude: Gtk.Entry = self.builder.get_object("etr_longitude") + self.etr_latitude: Gtk.Entry = self.builder.get_object("etr_latitude") + self.img_bar_times: Gtk.Image = self.builder.get_object("img_bar_times") + self.spb_periods_hour: list[Gtk.SpinButton] = [ + self.builder.get_object("spb_period_1_hour"), + self.builder.get_object("spb_period_2_hour"), + self.builder.get_object("spb_period_3_hour"), + self.builder.get_object("spb_period_4_hour"), + self.builder.get_object("spb_period_5_hour"), + self.builder.get_object("spb_period_6_hour"), + self.builder.get_object("spb_period_7_hour"), + self.builder.get_object("spb_period_8_hour"), + self.builder.get_object("spb_period_9_hour"), + ] + self.spb_periods_minute: list[Gtk.SpinButton] = [ + self.builder.get_object("spb_period_1_minute"), + self.builder.get_object("spb_period_2_minute"), + self.builder.get_object("spb_period_3_minute"), + self.builder.get_object("spb_period_4_minute"), + self.builder.get_object("spb_period_5_minute"), + self.builder.get_object("spb_period_6_minute"), + self.builder.get_object("spb_period_7_minute"), + self.builder.get_object("spb_period_8_minute"), + self.builder.get_object("spb_period_9_minute") + ] + self.lb_period_end: list[Gtk.Label] = [ + self.builder.get_object("lb_period_0_end"), self.builder.get_object("lb_period_1_end"), + self.builder.get_object("lb_period_2_end"), self.builder.get_object("lb_period_3_end"), + self.builder.get_object("lb_period_4_end"), self.builder.get_object("lb_period_5_end"), + self.builder.get_object("lb_period_6_end"), self.builder.get_object("lb_period_7_end"), + self.builder.get_object("lb_period_8_end"), self.builder.get_object("lb_period_9_end"), + ] + + + # Page 3: Behaviour + self.cb_picture_aspect: Gtk.ComboBox = self.builder.get_object("cb_picture_aspect") + self.sw_dynamic_background_color: Gtk.Switch = self.builder.get_object("sw_dynamic_background_color") + + + def show(self): + """ Display the window to the screen + """ + self.builder.get_object("window_main").show_all() + + # Smaller UI handling + if self.view_model.screen_height < self.view_model.breakpoint_ui: + self.img_tb_image_set.clear() + self.img_tb_heic_file.clear() + self.img_tb_source_folder.clear() + self.img_tb_network_location.clear() + self.img_tb_custom_location.clear() + self.img_tb_time_periods.clear() + + # Page 1: Image Configuration + self.add_items_to_combo_box(self.cb_image_set, self.view_model.image_sets) + self.image_source = self.image_source # This triggers the @image_source.setter + + # Page 2: Location & Times + self.add_items_to_combo_box(self.cb_network_provider, self.view_model.network_location_provider) + self.period_source = self.period_source # This triggers the @period_source.setter + + # Page 3: Behaviour + self.add_items_to_combo_box(self.cb_picture_aspect, self.view_model.picture_aspects) + self.set_active_combobox_item(self.cb_picture_aspect, self.view_model.cinnamon_prefs.picture_aspect) + self.sw_dynamic_background_color.set_active(self.view_model.cinnamon_prefs.dynamic_background_color) + + + # Show the main window + Gtk.main() + + + ############################################################ + # Observer # + ############################################################ + + + @property + def selected_image_set(self): + return self.view_model.cinnamon_prefs.selected_image_set + + @selected_image_set.setter + def selected_image_set(self, new_value): + # Save to the preferences + self.view_model.cinnamon_prefs.selected_image_set = new_value + + # Refresh images + image_names = self.view_model.get_images_from_folder(self.view_model.cinnamon_prefs.source_folder) + self.load_image_options_to_combo_boxes(image_names) + + # Image sets have the same names for the images: + # 9.jpg = Period 0 + # 1.jpg = Period 1 + # 2.jpg = Period 2... + for i in range(0, 10): + self.cb_periods[i].set_active(i + 1) + + + @property + def image_source(self): + return self.view_model.cinnamon_prefs.image_source + + @image_source.setter + def image_source(self, new_value): + self.view_model.cinnamon_prefs.image_source = new_value + + # Disable the wrong ToggleButtons + self.tb_image_set.set_active(new_value == ImageSourceEnum.IMAGESET) + self.tb_heic_file.set_active(new_value == ImageSourceEnum.HEICFILE) + self.tb_source_folder.set_active(new_value == ImageSourceEnum.SOURCEFOLDER) + + # Show or hide ListBoxRows + self.lbr_image_set.set_visible(new_value == ImageSourceEnum.IMAGESET) + self.lbr_heic_file.set_visible(new_value == ImageSourceEnum.HEICFILE) + self.lbr_source_folder.set_visible(new_value == ImageSourceEnum.SOURCEFOLDER) + + # Make the comboboxes invisible + for combobox in self.cb_periods: + combobox.set_visible(new_value != ImageSourceEnum.IMAGESET) + + + @property + def period_source(self): + return self.view_model.cinnamon_prefs.period_source + + + @period_source.setter + def period_source(self, new_value): + self.view_model.cinnamon_prefs.period_source = new_value + + self.tb_network_location.set_active(new_value == PeriodSourceEnum.NETWORKLOCATION) + self.tb_custom_location.set_active(new_value == PeriodSourceEnum.CUSTOMLOCATION) + self.tb_time_periods.set_active(new_value == PeriodSourceEnum.CUSTOMTIMEPERIODS) + + # Show/Hide the right ListBoxRows + self.lbr_network_refresh_time.set_visible(new_value == PeriodSourceEnum.NETWORKLOCATION) + self.lbr_network_provider.set_visible(new_value == PeriodSourceEnum.NETWORKLOCATION) + self.lbr_current_location.set_visible(new_value == PeriodSourceEnum.NETWORKLOCATION) + self.lbr_custom_location_longitude.set_visible(new_value == PeriodSourceEnum.CUSTOMLOCATION) + self.lbr_custom_location_latitude.set_visible(new_value == PeriodSourceEnum.CUSTOMLOCATION) + self.lbr_time_periods.set_visible(new_value == PeriodSourceEnum.CUSTOMTIMEPERIODS) + + self.refresh_charts_and_times() + + + + + ############################################################ + # UI Helper # + ############################################################ + + def set_active_combobox_item(self, combobox: Gtk.ComboBox, active_item: str): + """ Change active item in combobox by String value + + Args: + combobox (Gtk.ComboBoxText): ComboBox to set active + active_item (str): String item to set active + """ + list_store = combobox.get_model() + + for i in range(0, len(list_store)): + row = list_store[i] + if row[0] == active_item: + combobox.set_active(i) + + + def get_active_combobox_item(self, combobox: Gtk.ComboBox) -> str: + """ Request the current selected combobox label + + Args: + combobox (Gtk.ComboBox): ComboBox where to get value from + + Returns: + str: Selected value + """ + tree_iter = combobox.get_active_iter() + + model = combobox.get_model() + return model[tree_iter][0] + + + def add_items_to_combo_box(self, combobox: Gtk.ComboBox, items: list): + """ Add items to a combo box + + Args: + combobox (Gtk.ComboBox): ComboBox where to add the options + items (list): Possible options + """ + model = combobox.get_model() + store = Gtk.ListStore(str) + + for image_set in items: + store.append([image_set]) + + combobox.set_model(store) + + if model == None: + renderer_text = Gtk.CellRendererText() + combobox.pack_start(renderer_text, True) + combobox.add_attribute(renderer_text, "text", 0) + + + def load_image_options_to_combo_boxes(self, options: list): + """ Add a list of Strings to all image option comboboxes + + Args: + options (list): All possible options + """ + options.insert(0, "") + + for combobox in self.cb_periods: + self.add_items_to_combo_box(combobox, options) + + + def load_image_to_preview(self, image_preview: Gtk.Image, image_src: str): + """ Scales the image to a lower resoultion and put them into the time bar chart + + Args: + image_preview (Gtk.Image): Gtk Image where it will be displayed + image_src (str): Absolute path to the image + """ + try: + pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_src) + + # Scaling the images for smaller screens + if self.view_model.screen_height < self.view_model.breakpoint_ui: + pixbuf = pixbuf.scale_simple(221, 128, GdkPixbuf.InterpType.BILINEAR) + else: + pixbuf = pixbuf.scale_simple(260, 150, GdkPixbuf.InterpType.BILINEAR) + + image_preview.set_from_pixbuf(pixbuf) + except: + pass + + + def refresh_charts_and_times(self): + """ Refresh the charts and put them to the image views + """ + self.view_model.refresh_charts() + start_times = self.view_model.calulate_time_periods() + + for i in range(0, 10): + label_txt = self.view_model.time_to_string_converter(start_times[i]) + " - " + + if i != 9: + diff = timedelta(hours=start_times[i + 1].hour, minutes=start_times[i + 1].minute) - timedelta(minutes=1) + prev_time = time(hour=diff.seconds // 3600, minute=diff.seconds // 60 % 60) + label_txt += self.view_model.time_to_string_converter(prev_time) + else: + label_txt += "23:59" + + self.etr_periods[i].set_text(label_txt) + + # Load to the views + pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.view_model.TIMEBAR_URI_POLYLINES) + self.img_bar_images.set_from_pixbuf(pixbuf) + + pixbuf2 = GdkPixbuf.Pixbuf.new_from_file(self.view_model.TIMEBAR_URI) + self.img_bar_times.set_from_pixbuf(pixbuf2) + + + + + ############################################################ + # Callbacks # + ############################################################ + + # +-----------+-----------+---------------+ + # | Image Set | HEIC file | Source Folder | + # +-----------+-----------+---------------+ + + def on_toggle_button_image_set_clicked(self, button: Gtk.ToggleButton): + """ Clicked on ToggleButton "Image Set" + + Args: + button (Gtk.ToggleButton): Clicked ToggleButton + """ + if button.get_active(): + self.image_source = ImageSourceEnum.IMAGESET + + self.set_active_combobox_item(self.cb_image_set, self.selected_image_set) + + for i, combobox in enumerate(self.cb_periods): + selected_image_name = self.view_model.cinnamon_prefs.period_images[i] + self.set_active_combobox_item(combobox, selected_image_name) + + + def on_toggle_button_heic_file_clicked(self, button: Gtk.ToggleButton): + """ Clicked on ToggleButton "Heic file" + + Args: + button (Gtk.ToggleButton): Clicked ToggleButton + """ + if button.get_active(): + self.image_source = ImageSourceEnum.HEICFILE + + # Load images from source folder + files = self.view_model.get_images_from_folder(self.view_model.cinnamon_prefs.source_folder) + + if len(files) != 0: + self.load_image_options_to_combo_boxes(files) + + # Load the values for the images from the preferences + for i in range(0, 10): + self.set_active_combobox_item(self.cb_periods[i], self.view_model.cinnamon_prefs.period_images[i]) + else: + print("No image files!") + + + def on_toggle_button_source_folder_clicked(self, button: Gtk.ToggleButton): + """ Clicked on ToggleButton "Source Folder" + + Args: + button (Gtk.ToggleButton): Clicked ToggleButton + """ + if button.get_active(): + self.image_source = ImageSourceEnum.SOURCEFOLDER + + # Load the source folder to the view + # This will update the comboboxes in the preview to contain the right items + self.lbl_source_folder.set_label(self.view_model.cinnamon_prefs.source_folder) + + # Load files from saved source folder + files = self.view_model.get_images_from_folder(self.view_model.cinnamon_prefs.source_folder) + + if len(files) != 0: + self.load_image_options_to_combo_boxes(files) + + # Load the values for the images from the preferences + for i in range(0, 10): + self.set_active_combobox_item(self.cb_periods[i], self.view_model.cinnamon_prefs.period_images[i]) + else: + print("No image files!") + + + + # +------------------------------------+ + # | Select an image set | aurora ▼ | + # +------------------------------------+ + + def on_cb_image_set_changed(self, combobox: Gtk.ComboBox): + """ User select on of the included image sets + + Args: + combobox (Gtk.ComboBox): The used ComboBox + """ + if self.view_model.cinnamon_prefs.image_source == ImageSourceEnum.IMAGESET: + # Get the selected value + selected_image_set = self.get_active_combobox_item(combobox) + + # Store to the preferences + self.view_model.cinnamon_prefs.source_folder = \ + self.view_model.IMAGES_DIR + "/included_image_sets/" + selected_image_set + "/" + + self.selected_image_set = selected_image_set + + + # +----------------------------------------------+ + # | Select a heic file to import | (None) 📄 | + # +----------------------------------------------+ + + def on_fc_heic_file_file_set(self, fc_button: Gtk.FileChooser): + """ User has a heic file selected with the FileChooserDialog + + Args: + fc_button (Gtk.FileChooser): Parameter about the selected file + """ + # The the absolute path to the heic file + file_path: str = fc_button.get_filename() + + # Extract the heic file + result = self.view_model.extract_heic_file(file_path) + + # Update the preferences + self.view_model.cinnamon_prefs.selected_image_set = "" + self.view_model.cinnamon_prefs.source_folder = self.view_model.IMAGES_DIR + "/extracted_images/" + + # Load images only if the extraction was successfully + if result: + # Collect all extracted images and push them to the comboboxes + image_names = self.view_model.get_images_from_folder(self.view_model.cinnamon_prefs.source_folder) + self.load_image_options_to_combo_boxes(image_names) + else: + self.dialogs.message_dialog("Error during extraction!", Gtk.MessageType.ERROR) + + + # +------------------------------------------------------------+ + # | Select a source folder | 📂 Open file selection dialog | + # | /home/developer/Downloads/ + # +------------------------------------------------------------+ + + def on_btn_source_folder_clicked(self, button: Gtk.Button): + """ Button to choose an image source folder was clicked + + Args: + button (Gtk.Button): The clicked button + """ + folder = self.dialogs.source_folder_dialog() + files = self.view_model.get_images_from_folder(folder) + + # Update the preferences + self.view_model.cinnamon_prefs.selected_image_set = "" + self.view_model.cinnamon_prefs.source_folder = folder + "/" + + # Update the label + self.lbl_source_folder.set_label(folder) + + # Update the image comboboxes + self.load_image_options_to_combo_boxes(files) + + # Load the values for the images from the preferences + for i in range(0, 10): + self.cb_periods[i].set_active(0) + + if len(files) == 1: + self.dialogs.message_dialog("No image files found!") + + + def on_cb_period_preview_changed(self, combobox: Gtk.ComboBox): + """ User select an image from the ComboBox for the time period + + Args: + combobox (Gtk.ComboBox): The used ComboBox + """ + combobox_name = Gtk.Buildable.get_name(combobox) + period_index = int(combobox_name[10:11]) + + # Get the selected value + image_file_name = self.get_active_combobox_item(combobox) + + # Store selection to preferences + self.view_model.cinnamon_prefs.period_images[period_index] = image_file_name + + # Build up image path + image_path = self.view_model.cinnamon_prefs.source_folder + image_file_name + + self.load_image_to_preview(self.img_periods[period_index], image_path) + + + ## Location & Times + + def on_toggle_button_network_location_clicked(self, button: Gtk.ToggleButton): + """ User clicks on the ToggleButton for the network location + + Args: + button (Gtk.ToggleButton): Clicked ToggleButton + """ + if button.get_active(): + self.period_source = PeriodSourceEnum.NETWORKLOCATION + + self.spb_network_refresh_time.set_value(self.view_model.cinnamon_prefs.location_refresh_intervals) + self.set_active_combobox_item(self.cb_network_provider, self.view_model.cinnamon_prefs.network_location_provider) + + + def on_toggle_button_custom_location_clicked(self, button: Gtk.ToggleButton): + if button.get_active(): + self.period_source = PeriodSourceEnum.CUSTOMLOCATION + + self.etr_latitude.set_text(str(self.view_model.cinnamon_prefs.latitude_custom)) + self.etr_longitude.set_text(str(self.view_model.cinnamon_prefs.longitude_custom)) + + + def on_toggle_button_time_periods_clicked(self, button: Gtk.ToggleButton): + if button.get_active(): + self.period_source = PeriodSourceEnum.CUSTOMTIMEPERIODS + + for i in range(0, 9): + pref_value = self.view_model.cinnamon_prefs.period_custom_start_time[i + 1] + time_parts = [int(pref_value[0:pref_value.find(":")]), int(pref_value[pref_value.find(":") + 1:])] + + self.spb_periods_hour[i].set_value(time_parts[0]) + self.spb_periods_minute[i].set_value(time_parts[1]) + + + def on_spb_period_value_changed(self, spin_button: Gtk.SpinButton): + """ Callback if one of the time spinners (minute or hour) will be clicked + + (1) (2) (3) + Previous period Current period Next period + 12:34 - 14:40 14:41 - 16:20 16:21 - 17:30 + ^ + Variable to change + + Args: + spin_button (Gtk.SpinButton): SpinButton which was changed + """ + spin_button_name = Gtk.Buildable.get_name(spin_button) + index = int(spin_button_name[11:12]) - 1 + + # Determe time string and store to prefs + time_current_start = datetime(2024,1,1, int(self.spb_periods_hour[index].get_value()), int(self.spb_periods_minute[index].get_value())) + time_current_start_str = str(time_current_start.hour).rjust(2, '0') + ":" + str(time_current_start.minute).rjust(2, '0') + + self.view_model.cinnamon_prefs.period_custom_start_time[index + 1] = time_current_start_str + + + time_previous_end = time_current_start - timedelta(minutes=1) + self.lb_period_end[index].set_text(str(time_previous_end.hour).rjust(2, '0') + ":" + str(time_previous_end.minute).rjust(2, '0')) + + + self.refresh_charts_and_times() + + + def on_spb_network_location_refresh_time_changed(self, spin_button: Gtk.SpinButton): + """ User changed the refresh time of network location estimation + + Args: + spin_button (Gtk.SpinButton): The used SpinButton + """ + self.view_model.cinnamon_prefs.location_refresh_intervals = spin_button.get_value() + + + def on_cb_network_provider_changed(self, combobox: Gtk.ComboBox): + """ User changed the provider to estimate the location + + Args: + combobox (Gtk.ComboBox): The used ComboBox + """ + def network_refresh_thread(): + success = self.view_model.refresh_location() + + if success: + self.lb_current_location.set_text(\ + "Latitude: " + str(self.view_model.cinnamon_prefs.latitude_auto) + ", Longitude: " + str(self.view_model.cinnamon_prefs.longitude_auto)) + else: + self.dialogs.message_dialog("Error during fetching location. Are you connected to the network?", Gtk.MessageType.ERROR) + self.lb_current_location.set_text("Latitude: ?, Longitude: ?") + + + self.view_model.cinnamon_prefs.network_location_provider = self.get_active_combobox_item(combobox) + + thread = threading.Thread(target=network_refresh_thread) + thread.start() + + + def on_etr_longitude_changed(self, entry: Gtk.Entry): + """ User changes the value of the longitude Entry + + Args: + entry (Gtk.Entry): The manipulated Entry object + """ + try: + self.view_model.cinnamon_prefs.longitude_custom = float(entry.get_text()) + self.refresh_charts_and_times() + except: + pass + + + def on_etr_latitude_changed(self, entry: Gtk.Entry): + """ User changes the value of the latitude Entry + + Args: + entry (Gtk.Entry): The manipulated Entry object + """ + try: + self.view_model.cinnamon_prefs.latitude_custom = float(entry.get_text()) + self.refresh_charts_and_times() + except: + pass + + + # Behaviour + + def on_cb_picture_aspect_changed(self, combobox: Gtk.ComboBox): + """ User changes the value for the picture aspect ratio + + Args: + combobox (Gtk.ComboBox): The used ComboBox + """ + self.view_model.cinnamon_prefs.picture_aspect = self.get_active_combobox_item(combobox) + + + def on_sw_dynamic_background_color_state_set(self, _: Gtk.Switch, state: bool): + """ User switches dynamic background on or off + + Args: + _ (Gtk.Switch): Used Switch + state (bool): Current state + """ + self.view_model.cinnamon_prefs.dynamic_background_color = state + + + # About + + def on_cinnamon_spices_website_button_clicked(self, _: Gtk.Button): + """ Callback for the button to navigate to the Cinnamon Spices web page of this project + + Args: + button (Gtk.Button): Button which was clicked + """ + subprocess.Popen(["xdg-open", "https://cinnamon-spices.linuxmint.com/extensions/view/97"]) + + + def on_github_website_button_clicked(self, _: Gtk.Button): + """ Callback for the button to navigate to the GitHub web page of this project + + Args: + button (Gtk.Button): Button which was clicked + """ + subprocess.Popen(["xdg-open", "https://github.com/TobiZog/cinnamon-dynamic-wallpaper"]) + + + def on_create_issue_button_clicked(self, _: Gtk.Button): + """ Callback for the button to navigate to the Issues page on GitHub of this project + + Args: + button (Gtk.Button): Button which was clicked + """ + subprocess.Popen(["xdg-open", "https://github.com/TobiZog/cinnamon-dynamic-wallpaper/issues/new"]) + + + def on_ok(self, *args): + """ Callback for the OK button in the top bar + """ + try: + self.on_apply() + except: + self.dialogs.message_dialog( + "Error on apply the settings. Please check the settings and contact the developer.", + Gtk.MessageType.ERROR + ) + + # Close the window + self.on_destroy() + + + def on_apply(self, *args): + """ Callback for the Apply button in the top bar + """ + # Store all values to the JSON file + self.view_model.cinnamon_prefs.store_preferences() + + # Use the new settings + self.view_model.refresh_image() + self.view_model.set_background_gradient() + + + def on_destroy(self, *args): + """ Lifecycle handler when window will be destroyed + """ + Gtk.main_quit() \ No newline at end of file diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.png b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.png index 3c8c923d..4fb4316e 120000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.png +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.png @@ -1 +1 @@ -5.4/icons/icon.png \ No newline at end of file +5.4/res/icons/icon.png \ No newline at end of file diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.svg b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.svg index 9f41539c..cc60fb9a 120000 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.svg +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/icon.svg @@ -1 +1 @@ -5.4/icons/icon.svg \ No newline at end of file +5.4/res/icons/icon.svg \ No newline at end of file diff --git a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/metadata.json b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/metadata.json index 5b10ee3d..2ead12b6 100644 --- a/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/metadata.json +++ b/cinnamon-dynamic-wallpaper@TobiZog/files/cinnamon-dynamic-wallpaper@TobiZog/metadata.json @@ -1,9 +1,9 @@ { - "external-configuration-app": "preferences.py", + "external-configuration-app": "src/main.py", "uuid": "cinnamon-dynamic-wallpaper@TobiZog", "name": "Cinnamon Dynamic Wallpaper", "description": "Cinnamon extension for dynamic desktop backgrounds based on time and location", - "version": "2.1", + "version": "2.2", "multiversion": true, "cinnamon-version": [ "5.4", @@ -13,4 +13,4 @@ ], "max-instances": 1, "url": "https://github.com/TobiZog/cinnamon-dynamic-wallpaper" -} +} \ No newline at end of file