Fix PyPI installation issues

This commit is contained in:
A S Lewis 2020-03-28 07:35:24 +00:00
parent 35e70c91a6
commit 24efd5450b
216 changed files with 3191 additions and 58509 deletions

View File

@ -5,10 +5,5 @@ Authors ordered by first contribution:
(none yet) (none yet)
Image credits: Image credits:
Vectorgraphit <https://www.iconfinder.com/vectorgraphit> Carlo Rodriguez <https://www.iconfinder.com/icons/512534/exercise_fitness_gym_gymnasium_icon>
FatCow Web Hosting https://www.fatcow.com/>
Mr. Hopnguyen <https://www.iconfinder.com/Mr.hopnguyen>
Other credits:
Tartube is partially based on youtube-dl-gui

964
CHANGES
View File

@ -1,965 +1,9 @@
v2.0.0 (29 Feb 2020) v1.002 (28 Mar 2020)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
MAJOR NEW FEATURES - Fix for PyPI installation problems
- Tartube can now be installed from PyPI, or by using the new DEB/RPM packages
(Linux/BSD only; installation from PyPI does not work on MS Windows)
- DEB/RPM packages marked 'STRICT' are also available for uploads to
repositories with lots of rules, such as the official Debian repository.
In 'STRICT' packages, updating youtube-dl from within Tartube is disabled.
The 'STRICT' packages are compiled using new environment variables,
TARTUBE_PKG and TARTUBE_PKG_STRICT (replacing the old TARTUBE_DEBIAN)
environment variable. See the comments in setup.py for more details
- During a download operation, in the Progress Tab, you can now right-click a
video and select 'Stop after these videos'. This allows all of the current
video downloads to finish, before halting the download operation
- The download options window (in the Formats tab) did not allow users to
select an audio format before selecting a video format. The reason for this
restriction was that youtube-dl did not download the right formats, if an
audio format was selected first. Unfortuantely, it prevented users from
downloading a separate audio file, when this was available (e.g. an .m4a
file from YouTube). The restriction has now been removed; instead, Tartube
will automatically reorder the specified video/audio formats, so that video
formats are passed to youtube-dl first
MAJOR FIXES v1.0 (27 Mar 2020)
- If an upload operation is automatically performed before a download
operation, and if the user tried to download a single video/channel/
playlist/folder, everything was downloaded instead of the single video/
channel/playlist/folder. Fixed
- Fixed an error in the 'Show system command' dialogue window, that prevented
it from opening at all
- Fixed parsing of download options inside double quotes "..."
MINOR NEW FEATURES
- Added a 'Cancel' button to some dialogue windows that didn't already have one
- Added a copy of the XDG module to the Tartube code, so it is no longer
necessary to install it before running/installing Tartube (Linux/BSD only)
MINOR FIXES
- Fixed a system error during a forced youtube-dl update (MS Windows only)
- Fixed wrong location for config file backups (MS Windows only)
- Fixed wrong location for Tartube temporary/test folders (all systems)
- Fixed missing (or duplicate) dialogue windows after failing to load the
config file and/or database file, in some rare situations
- The config file could not be created if its parent directory did not exist;
fixed
- Fixed loading of the wrong database file, in some rare situations
- Removed the old 'hello world' code intended for testing on MS Windows; it's
no longer required
- If Tartube can't find its icon files, a simple error message is now generated
rather than a long traceback
v1.5.0 (22 Feb 2020)
------------------------------------------------------------------------------- -------------------------------------------------------------------------------
This is the first release candidate for v2.0.0. - This is the first public release of GymBob
MAJOR NEW FEATURES
- You can now run multiple instances of Tartube on your system at the same
time. Multiple instances cannot load the same Tartube database; they must
each load their own database. Tartube will now remember the databases it
has loaded. If there are three databases (perhaps one on your main hard
disk and two on an external drive), you can start Tartube three times, and
they will each load a different database. This behaviour can be configured,
if necessary. Click 'Edit > System preferences... > Filesystem > Database'
- HookTube acts as a redirection service for YouTube. Because of lawyers and
their evil machinations, HookTube's functionality is not as extensive as it
once was. Added the Invidious website (https://invidio.us/) as an
alternative
- Added custom downloads. To start a custom download, click 'Operations >
Custom download all', or right-click a video/channel/playlist/folder. A
custom download is just like a normal download, until you customise it. To
do that, click 'Edit > System preferences > Operations > Custom'. Custom
downloads can be used to divert YouTube requests to HookTube or Invidious,
and to insert a delay between video downloads when the website is
complaining about robots
- Added a new toolbar at the bottom of the main window, below the list of
videos. The toolbar is hidden, by default. To reveal it, click the 'Show
filter options' button in the bottom right-hand corner. Buttons in the new
toolbar can be used to sort the videos alphabetically, rather than by date,
and to search for videos whose name matches a string (or regex). The button
to search for videos by date has been moved into this toolbar
- If you find a video that can't be downloaded, and you're not sure why, you
can now perform a test download. First, click 'Operations > Test
youtube-dl...' (or right-click a video in the main window's list). Copy the
video's URL into the dialogue window, and then click the OK button. Click
the Output Tab to see the results. If the test successfully downloads the
video, then the problem was with Tartube. If the test fails to download the
video, then the problem is with the underlying youtube-dl software (or with
the video website)
- During a test, it's possible to omit the video URL, while specifying some
youtube-dl download options. For example, you could fetch the youtube-dl
version number with the option --version
- Added a new operation for tidying up files in Tartube's data directory
(folder). To start the operation, click 'Operations > Tidy up files...'.
You could also right-click a channel and select 'Channel actions > Tidy up
channel', and so on. A dialogue window appears, in which you can specify
which files should be tidied up. Choose carefully, because any files
deleted as a result of this operation cannot be recovered
- The main window's switch button (in the toolbar near the top of the window)
now has six settings, rather than four. Click the button repeatedly to
cycle through them
- Interesting and important videos can now be bookmarked (e.g., by right-
clicking a video and selecting 'Mark video > Video is bookmarked').
Bookmarked videos are visible in the new 'Bookmarks' folder. Bookmarking is
an alternative to favourites; bookmarks usually apply to a single video,
whereas favourites usually apply to a whole channel, playlist or folder
- Also added a new 'Waiting Videos' folder. This acts as your own private
playlist - a list of videos that are waiting to be watched. To make a video
visible in this folder, right-click it and select 'Mark video > Video is in
waiting list'. When you watch the video, it will automatically disappear
from the 'Waiting Videos' folder (this doesn't happen to bookmarked videos)
- The previous version was unable to delete a channel, playlist or folder (see
below). That error caused a partially-deleted channel/playlist/folder to
appear in the Videos Tab, on the left-hand side. In case similar errors
occur in the future, a feature has been added to look for errors and
inconsistencies in the Tartube database and automatically fix them. Click
'Edit > System preferences... > Filesystem > DB Errors > Check' to use it
- Tartube can now fetch a list of available video formats for a video. Right-
click the video and select 'Fetch > Available formats'. Click the Output
Tab to see the results
- Tartube can also fetch a list of available subtitles for a video. Right-click
the video and select 'Fecth > Available subtitles'. Click the Output Tab to
see the results
- Tartube can now remember the size of its main window, and use the same size
when it restarts. This feature is disabled by default. To enable it, click
'Edit > System preferences... > Windows > Main window > Remember the size
of the main window when shutting down'
MAJOR FIXES
- In the previous version, Tartube was unable to delete a channel, playlist or
folder. Fixed
- Some procedures took an extremely long time. For example, after right-
clicking a channel and selecting 'Channel contents > Mark videos as new',
the procedure could take several minutes if the channel had hundreds of
videos, or several hours if it contained thousands of videos. The faulty
code has been fixed, and the procedure now takes just a few seconds, even
for many thousands of videos
- Videos in a Tartube folder (for example the 'Unsorted Videos' folder) were
added to the download list in a 'Check all' operation, even when they had
been checked before. This no longer happens, by default. To restore the
original behaviour, click 'Edit > System preferences... > Operations >
Downloads > For simulate downloads, don't check a video in a folder more
than once' to deselect it
- Fixed the button for finding videos by date, which was not working at all in
the previous version
- Fixed an occasional 'signal is not defined' error when the user stops an
operation (for example, a download operation)
- Various inconsistencies in the way alternative download destinations are
handled have all been fixed. Download operations sometimes freezed
indefinitely, because Tartube doesn't download two channels/playlists/
folders with the same download destination at the same time. The code has
been updated to prevent the freeze from ever happening again
- Fixed some more crashes caused by Gtk during a download operation
MINOR NEW FEATURES
- Videos downloaded into a temporary folder are deleted when Tartube restarts.
After shutting down Tartube, users often like to copy these videos
somewhere else on their hard drive. You can now ask Tartube to open the
temporary directories (folders), before shutting down, which will remind
you to do something with the videos. To enable this behaviour, click
'Edit > System preferences... > Filesystem > Temporary folders'
- In the main window's list of videos, the date is now displayed as 'today' and
'yesterday' when possible. This behaviour can be disabled in 'Edit > System
preferences... > Windows > Main window'
- During refresh operations, a progress bar is now visible in the bottom-left
corner of the window (just like the one visible during a download
operation)
- When setting an alternative download destination for a channel/playlist/
folder (for example, by right-clicking a channel and selecting 'Channel
actions > Set download destination...'), the dialogue window has been
updated to show the previously selected alternative at the top of the list.
This should save a lot of time when setting the alternative download
destination for many channels/playlists/folders
- The alternative download destination, if any, is now visible in the tooltips
for the channel/playlist/folder
- Improved the appearance of the dialogue windows seen when Tartube runs for
the first time
- Tweaked the appearance of the list of channels/playlists/folders in the
main window, so that for items with long names, more text is visible
MINOR FIXES
- Fixed a 'No such file or directory' error seen during a download operation,
if an external hard drive suddenly become disconnected (for example, if
the cable falls out)
- Fixed rare problems in loading Tartube's config file
- After a download operation, the list in the top half of the progress tab
often had one or two items in it, even when 'Hide active rows after they
are finished' was selected. Fixed
- The length of lines of text, and spacing between lines, in various dialogue
windows has been made uniform
- Improved the appearance of the main window by adding frames around everything
- Renamed some misnamed icon files. The old icon files were being used in the
MS Windows installer, so fixed that too
- If the user performed two successive refresh operations, the second one
halted after a couple of seconds. Fixed
- When videos are deleted from Tartube's database, any post-processing
artefacts are now deleted with them
- Fixed a few incorrect regex-matching actions
- The user can specify that the main 'Download all' button should be
desensitised, but the setting was not applied correctly after Tartube
restarted. Fixed
- Removed a duplicate menu option in the Video Index popup menu
- Tartube channels, playlists and folders keep counts of the number of videos
inside them, including the number of favourite videos, downloaded videos,
and so on. The code was not working correctly, so the counts were not
always accurate. This version updates the code and recalculates all of the
counts
- Fixed folder icons with an incorrect colour in various edit windows
- Fixed markup errors for videos whose URL contained an ampersand character
- Fixed the Gtk warning when closing the 'Add new video(s)' dialogue window
- Updated the installer scripts for MS Windows, so they don't try to update the
Windows registry (the code has never worked)
- You can no longer set videos as favourite, or new (etc), in an empty channel,
playlist or folder
- In the video list, labels can be right-clicked to copy a video's location
(for example, so it can be copy-pasted somewhere else). This did not work
the same way for every clickable label, and in some cases did not work at
all. Fixed
- Tooltips for videos contained &amp; rather than a simple ampersand character.
Fixed
- Tartube debug messages for the mainapp.py file (which can only be enabled
by editing the file) now have a second debug flag, so the timer functions
can be filtered out
- Checked all keyboard shortcuts to remove duplicates
v1.4.0 (2 Feb 2020)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- The structures of files and directories (folders) in Tartube's data
directory (into which all videos are downloaded) has been changed in
response to Git #28. Tartube will be able to recognise both structures
forever, so there is no need to move anything around on your computer. (If
you actually want to move things around, see the README file)
- Creating a channel/playlist/folder starting with a full stop (period) is no
longer allowed; some channels/playlists/folders might be automatically
renamed when you open Tartube
- The edit and preference windows have been reorganised, adding a second
layer of tabs in many windows. This should hopefully make things a little
easier to find
- In the download options window, you can now specify multiple languages for
your subtitles, instead of just one (Git #47)
- Added some more filename formats (in Edit > General download options...
> Files > File names). When downloading a partial playlist (for example,
starting at the 5th video), youtube-dl cannot create files with the correct
number (naming the first file downloaded #1, instead of #5). Tartube can
now handle this correctly. In the drop-down box, use one of the formats
containing 'Autonumber' (Git #47)
- You can now limit the length of a download operation. This is particularly
useful on small devices, or when leaving Tartube to run overnight. Click
Edit > System preferences... > Scheduling > Stop, and choose one or more of
the new options (Git #47)
- When adding new videos, channels or playlists, you can now turn on clipboard
monitoring. Simply select a URL (for example, in your web browser), press
CTRL+C to copy it to your system's clipboard, and then Tartube will
automatically paste it into the dialogue window (Git #52)
- The MS Windows installer now includes a copy of AtomicParsley, so there is no
need to install it yourself. This does not affect Linux/BSD users, who can
continue installing AtomicParsley by the usual methods
- The list in the top half of the Progress Tab is often full, and it's
sometimes difficult to see what is being downloaded right at this moment.
You can now hide finished rows, if you want to, so that active rows appear
at the top of the list
MAJOR FIXES
- The Gtk graphics libraries have historically been full of bugs, which made
applications using Gtk unstable. Most of these bugs are fixed, but the
fixes can take years before they propogate into operating systems. If Gtk
v3.22 (or lower) is installed on your system, Tartube automatically
disables some minor cosmetic features to prevent crashes. If you are using
Gtk v3.24 or later, and are still experiencing unexplainable crashes, you
can now disable the cosmetic features regardless of Gtk version. Click
Edit > System preferences... > General > Modules, and select 'Assume that
Gtk is broken...'
- On Linux/BSD, attempts to update youtube-dl from the Tartube menu so,etimes
produced a 'permission denied' error. There are now new settings available
in 'Edit > System prefences... > youtube-dl > Shell command for update
operations'. If you installed youtube-dl using pip/pip3, the 'recommended'
options should now work, if they didn't work before. Some pip3 warning
messages, which caused Tartube to think the update had failed, are now
filtered out
- A user complained that his Tartube database file had been corrupted. We are
still not sure what the cause was, but the code has been changed to make
that kind of corruption impossible
- Fixed some occasional crashes when, during a download operation, Tartube
tried to sort the videos in the selected channel/playlist/folder
- When switching databases, if Tartube couldn't load the new database, it tried
again after being restarted, rather than trying to load the previous
(readable) database. This has now been fixed
- Some youtube-dl download options could be applied to playlists, but not
channels, even though youtube-dl allows them to be applied to be both.
Fixed, and updated some labels to make it clearer what the options are for
(Git #47)
- In all edit windows, the 'Apply' button at the bottom of the window did not
work. Fixed
MINOR NEW FEATURES
- Tartube icons have been updated, in some cases making them easier to identify
- In the Progress Tab, added tooltips to assist with identifying undownloaded
videos (Git #51)
- More types of YouTube error message can now be filtered out
- We have also added a customisable list of strings (or regular expressions);
if set, any matching error/warning messages (on any website) are filtered
out
- In the Video Index popup menus, 'rename default location' has been changed
to a much more comprehensible 'rename channel', etc
- You can now open a video in its system directory (folder) by right-clicking
it, and selecting 'Show location'
- You can now switch databases from the main menu. Click File > Change database
(which opens the preference window at the correct page; hopefully this is
quicker than trying to find the right page yourself)
- There was no way to save Tartube's config file (except by shutting down
Tartube). To do that, you can now click File > Save all
- If Tartube is unable to read the config file and/or database file, the text
in the resulting dialogue windows has been improved. In some circumstances,
multiple dialogue windows were produced; this has now been fixed
- In the download options window, the option to 'embed subtitles with video'
now appears in two different places, to make it easier to find (Git #47)
- If the 'Add new video(s)', 'Add a new channel' or 'Add a new playlist'
dialogue windows are open, you can now drag-and-drop into them (Linux/BSD
only). Modifications to the code mean that it's no longer possible to
drop one URL into the middle of an existing one, rendering both of them
useless
- Tartube checked URLs for validity before adding them, but this did not work
as well as intended. The code has been improved, so less garbage should
appear in the 'Add new video(s)' dialogue window, and so on
- You could already download a temporary copy of video(s) by right-clicking
them and selecting 'Temporary > Download', but that can be inconvenient
for multiple videos, as you had to wait for each download to finish. You
can now select 'Temporary > Mark for download' instead, which creates a
copy of the video in the 'Temporary Videos' folder. When you're ready to
download them all, just download that folder
- Minor improvements to aesthetics for some textviews and treeviews
MINOR FIXES
- Fixed incorrect operation of the checkbuttons in the Errors/Warnings Tab.
Added new checkbuttons to separate Tartube errors/warnings from youtube-dl
errors/warnings (Git #50)
- 'Child process exited with non-zero code' errors still appeared in the
Errors/Warnins tab, even if the user has disabled them. Fixed
- Tooltips for videos could not be enabled/disabled if no channel/playlist/
folder was selected. Fixed
- On MS Windows, edit/preference windows will no longer increase in size, if
there isn't enough room for each window's tabs
- Fixed rare 'Permission denied' errors when trying to create a directory
(folder) on the filesystem
- In the download options edit window, the combobox for audio formats had
multiple and ever-increasing empty spaces. Fixed
- In the download options window, File > File names, the default value for the
custom format was garbled. Fixed, and it should now be working as intended
- During a simulated download, videos which are not in a channel or playlist
(for example, videos in the 'Unsorted Videos' folder) did not appear in
the Results List in the Progress Tab. Fixed
- Fixed an unprintable character in the licence declaration, visible in
Tartube's 'About' window
- When deleting a video, Tartube will now delete more related files (such as
those produced when post-processing a video)
- Removed a few duplicate ISO 639-1 language codes
v1.3.077 (26 Jan 2020)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- Drag and drop (for example, from a web browser into Tartube's main window)
is now fully working on Linux/BSD. On MS Windows, drag and drop does not
work at all for any Gtk application. It is unlikely that the Tartube
authors can do anything about this (Git #35)
- The 'Add new video(s)' dialogue window can now handle URLs representing
channels and playlists, as well as URLs representing individual videos.
During a download operation, if Tartube is expecting an individual video
but receives a channel/playlist, it will automatically create a new
channel, and download videos into that channel. You can change this default
behaviour, if you want (Edit > System preferences... > URL flexibility
preferences)
- To change the name of the new channel/playlist, right-click it and select
'Filesystem > Rename default location...'
- If Tartube creates a channel, which should really be a playlist, then you
can now convert one to the other. Right-click a channel and select
'Channel actions > Convert to playlist'. Right-click a playlist and select
'Playlist actions > Convert to channel'
- In the download options windows, it's now very easy to tell Tartube to
convert videos to sound files. Open the window by clicking 'Edit >
General download options...', click the 'Hide advanced download options'
button if necessary, click the 'Sound only' tab, select your preferences,
and apply them by clicking the OK button at the bottom of the window
- You can now see the download options applied to a video, channel, playlist
or folder without having to download anything. Right-click a video/channel/
playlist/folder and select 'Downloads > Show system command'
- During a download operation, the system commands used are now visible (by
default) in the Output Tab. The system command can also be displayed in the
terminal, if required; this is disabled by default
MINOR NEW FEATURES
- In the Output Tab, the summary page is now hidden by default. To make it
visible, click 'Edit > System Preferences... > Output >
Show a summary of active threads' and then restart Tartube
- In the Errors/Warnings Tab, added checkbuttons to filter out errors and/or
warning messages, if required (Git #50)
- In the Progress tab, in the top half of the window, you can now right-click
an unnamed video to open it in your web browser. This will be useful in
identifying videos that did not download, and whose name is unknown to
Tartube (Git #51)
- Columns in the Progress tab have been rearranged a little, so that the
user can more easily see how quickly the download is progressing, when
Tartube's main window is small
MAJOR FIXES
- Fixed multiple issues with Tartube, when running under Python 3.8
- Replaced all remaining references to the Python os.rename() function, which
can cause crashes on some filesystems (Git #34)
- Fixed crashes caused by the new YouTube error messages (January 2020), which
some versions of youtube-dl cannot handle correctly
- Fixed issues with the default location for videos, again. Fixed an issue
with adding folders inside the currently selected folder (Git #36, #46)
MINOR FIXES
- Fixed various Gtk warning messages, visible only on some systems
- Videos whose name contains an ampersand (&) character could not be opened by
clicking the 'Media player' label in the Video Catalogue. Fixed
- The properties windows for videos, channels and playlists showed a folder
icon, instead of a video/channel/playlist icon. Fixed
- The popup menu in the Progress tab, in the top half of the tab, did not work
as intended during a download operation, and again after a download
operation. Fixed both sets of issues
- Coloured text was not displayed in the Output Tab correctly. Fixed
v1.3.048 (23 Jan 2020)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- Tartube now creates an icon in the user's system tray. Closing the main
window now closes to the tray, by default. To disable this behaviour,
click Edit > System preferences > Windows > Deselect 'Close to the tray...'
- When that functionality is enabled, Tartube can be shutdown by clicking
File > Quit. Scheduled download operations will still take place if Tartube
has been closed to the tray. Implements Github issue #37
- Tartube can now show a desktop notification at the end of a download
operation, rather that a dialogue window. This does not work on MS Windows.
On other operating systems, enable desktop notifications by clicking
Edit > System preferences... > Operations > Show a desktop notification...
- When you click the 'Add new video(s)' button, the folder displayed in the
dialogue window is now the same folder that's selected in the main window
(if any). The same applies for adding channels, playlists and folders.
Fixes Github issue #36
- If you normally use the 'Check all' button rather than the 'Download all'
button, and if you want to download a temporary copy of one of the videos,
there's now an easier way to do it. In the Videos tab, right-click the
video, and select 'Temporary > Download' or 'Temporary > Download and
Watch'. A copy of the video is downloaded into the 'Temporary Videos'
folder, without affecting any other folders
MINOR NEW FEATURES
- The icons for channels and playlists have been replaced, to make it easier to
tell them apart. Some other icons have been replaced too
- Videos can now be dragged and dropped from a web browser (or similar
application) into Tartube's main window, which automatically adds the video
to the currently selected folder (or 'Unsorted Videos', if no folder is
selected). Unfortunately, the code is not yet working reliably. We are
looking for a solution (Github issue #35)
- The layout of the Format tab in the download options window has been improved
to alleviate confusion experienced by users trying to download a video to
a sound format such as .mp3 (only). See the new section in the README file
- A number of new video/audio formats have been added, for example several new
60fps formats, implementing Github issue #40
- When you apply download options to a video/channel/playlist/folder, the
options are now cloned from the default set of options (those visible in
Edit > General download options...). To disable this behaviour, click
Edit > System preferences... > Operations > When applying download options,
automatically clone general download options. Implements Github issue #39
- The options already applied to a video/channel/playlist/folder can now be
reset to match the general options, any time you want. Use the new button
at the bottom of the download options window, in the General tab
MAJOR FIXES
- Fixed a rare crash when the video's JSON filename was too long for the
operating system
- In the download options window, Formats tab, the user can add up to three
video formats. The third format, if added, was always ignored. Fixed
MINOR FIXES
- If you perform a refresh operation on a folder, the operation now applies to
all videos, channels, playlists and folders inside it
- After adding a video to the folder that's currently selected, the video does
not appear immediately in the video catalogue. Fixed
- In the Progress and Errors/Warnings tabs, the column headers scrolled away
along with the rest of the list. Fixed; they are now always visible
- Video nicknames were not set correctly after an update operation. Fixed
- During a refresh operation, a video's name was compared against the full
filepath (filename and extension), which produced none of the intended
matches. Fixed
- The edit/preference windows had a tendency to increase in size without
limits. Fixed
- A video's annotations.xml file was not deleted correctly, when required.
Fixed
- In the download options window, the option 'hls-prefer-ffmpeg' is now working
correctly
- In the download options window, the 'prefer avconv over ffmpeg' options have
been desensitised on MS Windows, as there is no known method of using
Tartube with avconv on MS Windows
- youtube-dl creates a file, ytdl-archive.txt, recording all the videos that
it has downloaded. This can interfere if the user tries to re-download the
video(s) for any reason. Create of the ytdl-archive.txt file can now be
disabled (Edit > System preferences... > youtube-dl > Deselect 'Allow
youtube-dl to create its own archive...')
- If creation of the archive file is nonetheless enabled, Tartube can now
re-download video(s) without problems
- In rare circumstances, Tartube was unable to redraw the video catalogue
(the right-hand side of the Videos tab). Fxied
v1.3.007 (20 Dec 2019)
-------------------------------------------------------------------------------
MAJOR FIXES
- v1.3.007 was completely broken when replacing an earlier installation. Fixed
- When Tartube's data directory was copied from one place to another (for
example, from one external drive to another), Tartube did not adapt to the
change very well. The way file paths are stored in Tartube's database has
been changed to eliminate this problem
MINOR FIXES
- Fixed an invalid time value which (sometimes) prevented a refresh operation
from completing correctly
v1.3.0 (20 Dec 2019)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- Tartube on MS Windows did not recognise FFmpeg or AVConv. You can now tell
Tartube to download and install a compatible version of FFmpeg from the
main menu (Operations > Install FFmpeg). Tartube still cannot recognise
the ordinary version of FFmpeg, and it still does not recognise AVConv at
all. It is unlikely that this situation can be remedied
- A new Output Tab has been added, in which you can see what is happening
internally when you check or download videos, update youtube-dl, install
FFmpeg, or refresh the Tartube database. The amount of information shown
can be customised in the System preferences window. The information can
still be written to STDOUT/STDERR, if required
- For users on other operating systems, the system preferences window
displayed the wrong location of the FFmpeg/AVConv executable. This has now
been fixed
- There are now two simple ways to specify the video resolution you want to
download (for example, 1080p). You can use the download options window
(Edit > General download options... > Formats, and then choose a video
format like 'any format [1080p]'). You can also use the new spinbutton at
the bottom of the Progress Tab. Both of these methods have the same effect,
so it's not necessary to use both of them. Tartube will download videos in
that resolution if possible, or in the next highest available resolution
otherwise
- The download options window has been simplified, with only the most useful
options visible. If you want to see the full range of options, open any
download options window, and in the General tab, click the new 'Show
advanced download options' button
MINOR NEW FEATURES
- By default, temporary folders are no longer emptied when Tartube shuts down,
but only when Tartube starts up. This means you can continue watching
temporary videos you've downloaded even after shutting down Tartube. If you
want temporary folders to be emptied on shutdown, as before, select Edit >
System preferences... > Videos > Empty temporary folders when Tartube shuts
down
MAJOR FIXES
- Tartube experienced a whole range of problems when downloading videos to a
hard drive that was running out of space. Tartube now checks the available
disk space before starting to download anything, and continues checking it
throughout the download process. You can specify how much disk space should
be available in the System Preferences window. If the hard drive, despite
your best efforts, actually does run out of space, Tartube is now much more
resilient (and can usually halt the download process, rather than
crashing). The amount of disk space available is now visible in the System
Preferences window
- Adding a channel/playlist/folder whose name included a slash, for example
'Adam/Eve's Channel', had unfortunate consequences, with Tartube creating
a directory (folder) at the wrong location. Slashes are now automatically
converted to hyphens, which solves the problem
- In Tartube's window, dragging a channel/playlist/folder to a new location in
the tree changes the hard drive, moving a directory (folder) to a new
location in the filesystem. If a directory (folder) with the same name
already existed at that location, an invisible error occurs. Tartube now
displays a visible error so the user can delete the duplicate directory
(folder) manually
- When refreshing the Tartube database (e.g. Operations > Refresh database),
the moviepy module freezes if it encounters a corrupted video file. We
can't fix the moviepy module, but the Tartube code has been made much more
resilient
- When refreshing the Tartube database, Tartube made bad decisions if it was
looking for a video called 'ymca.mp4', but found a video called
'ymca.webm'. This has been fixed
- Tartube is now able to detect if its data directory (into which videos are
downloaded) doesn't exist. Usually this is because an external hard drive
has not been mounted; the user is now warned about this, so they can mount
it
- On MS Windows, if the user has updated youtube-dl or installed FFmpeg,
Tartube no longer freezes on shutdown
- On MS Windows, Tartube was unable to open a video file in the system's
default media player, if the name contained an ampersand. Fixed
MINOR FIXES
- When deleting large channels/playlists/folders, sometimes not everything was
deleted, and the user had to delete the item a second time. This was due to
inconsistencies in the Tartube database, which have now been fixed
- Channels/playlists/folders beginning with a number, e.g. '5 Pewdiepie', were
supposed to be displayed in numerical order, rather than in strict
alphabetical order. This did not work as intended (e.g. '11 Pewdiepie' was
listed before '1 T-Series'). Fixed again
- In the 'Delete channel' dialogue window (and so on), the name of the channel
to be deleted is now displayed prominently
- The size of the MS Windows installer has been reduced by about 40%
v1.2.008 (30 Sep 2019)
-------------------------------------------------------------------------------
MINOR NEW FEATURES
- Tartube now ignores the YouTube 'WARNING: video doesn't have subtitles'
by default. You can change this setting, if you want to
MAJOR FIXES
- When moving a channel/playlist/folder to a different place on your
filesystem, or when renaming a channel/playlist/folder, in certain rare
situations data in the Tartube database isn't updated correctly. This may
lead to a freeze or a crash. I'm not sure yet what the cause is, but I have
added temporary code to prevent the problem affecting any user
- Fixed error messages generated when checking/downloading individual
channels/playlists/folders
- Fixed faulty code for importing videos/channels/playlists/folders into the
database
v1.2.0 (31 Aug 2019)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- Multiple channels, playlists and/or folders can now download their videos to
a single location. The README.rst file explains how it works, and why you
might want to do it
- You can also tell Tartube to download all videos into the 'Unsorted Videos'
or 'Temporary Videos' folders, instead of downloading them into separate
directories/folders for each channel and playlist
- Added automatic deletion of videos, disabled by default. Before enabling it,
you should do a 'Check all' or 'Download all' operation, which will create
the necessary youtube-dl archive files
- Added archiving. A video, channel, playlist or folder that is marked
archived won't be auto-deleted (but can still be deleted manually by the
user)
- You can now disable both checking and downloading a channel, playlist or
folder, if you want to. (It was already possible to just disabled
downloading them)
- Download operations can now be scheduled to take place at regular intervals
- You can now 'Download and watch' a video. The video is opened in your
system's default media player as soon as it has been downloaded
- Tartube can now download a video's annotations file automatically. Warnings
generated by YouTube about the lack of annotations are ignored by default
- For channels/playlists/folders containing many videos, you can now skip to
the first video uploaded after a certain date, using the new button in the
toolbar at the bottom of the Videos Tab
- The lists in the Results Tab can now be right-clicked, so you can change the
order in which videos/channels/playlists/folders are checked/downloaded,
abandon a download, play a video directly from the Results List, delete a
video directly from the Results List, and so on
- The edit window for youtube-dl options has been improved, adding many new
options to the GUI interface
- Plain text exports of Tartube's database can now be re-imported. Some
inconsistencies with the import process (from JSON and plain text files)
have been fixed
MINOR NEW FEATURES
- Added tooltips in several places. If you don't want to see tooltips above
videos/channels/playlists/folders, you can turn them off
- You can now select multiple videos in the Video Catalogue, and apply an
action to all of them (by right-clicking them)
- You can now switch to smaller icons in the Video Index (on the left side of
the Videos Tab), if you want to
- You can now force the Video Index to expand its tree whenever you click on a
folder, revealing any channels/playlists it contains. This is disabled by
default
- If you only want to check videos, and never download them, you can disable
the 'Download all' buttons. Individual videos/channels/playlists/folders
can still be downloaded by right-clicking them
- Tartube's file structure has changed. If you run it from the command line,
you might need to use a (slightly) different command. See the README.rst
for details of what command to use
- The path to the FFmpeg/AVConv executable can now be specified by the user.
This will be especially helpful for MS Windows users
- Columns in the Progress Tab can now be manually resized
- The Tartube website can now be opened from the main window menu
- XDG has been added as an optional dependency, for the benefit of Debian
packagers
MAJOR FIXES
- The MS Windows installer should now work for everyone
- Refresh operations are now stable (should not crash) on systems with Gtk 3.22
or earlier
- When downloads were disabled for a folder, downloads for channels/playlists/
folders inside that folder were still enabled. This is counter-intuitive,
so disabling downloads for a folder disables downloads for everything it
contains
- Marking/unmarking a video as favourite caused certain problems, which should
now be fixed
MINOR FIXES
- Fixed some unicode errors in reading JSON and plain text files
- Fixed the wrong page size displayed in the toolbar at the bototm of the
Videos Tab
- Empty lines in a video's description are now preserved when they're displayed
in Tartube's main window
v1.1.0 (18 Aug 2019)
-------------------------------------------------------------------------------
MAJOR NEW FEATURES
- You can now create an export of Tartube's database. This export contains
details of videos, channels, playlists and/or folders, but not the videos
themselves (or any of the thumbnail/description/metadata files). The
export can take two forms: JSON data, or plain text. If JSON data, the
exported file can later be imported into another Tartube database (imports
from plain text are not implemented yet). You can export either the
entire database, or just one channel/playlist/folder (and everythng it
contains)
- You can now change the name of a channel, playlist of folder. This doesn't
have any effect on your filesystem; it only changes the name displayed in
the Video Index (the left-hand side of the Videos Tab). This might be
useful for channels and playlists that have weird or very short names. For
example, right-click a folder and select 'Folder actions > Set nickname...'
- You can also rename the channel, playlist or folder, and this action DOES
affect the filesystem, changing the directory/folder on your hard drive
where the channel/playlist/folder videos are stored. For example, right-
click a folder and select 'Filesystem > Rename location...'
- If you change the format of a downloaded video file from the default 'Title'
to, for example, 'Title + ID', the Video catalogue (the right-hand side of
the Videos Tab) will now simply display the video's title (which should be
easier to read). To see the actual filename, you can right-click the video
and select 'Show properties'. This only works if the video's metadata was
downloaded when the video itself was downloaded; this is now turned on by
default for all new users
MINOR NEW FEATURES
- When adding videos, channels and playlists, the contents of the system's
clipboard was automatically copied into the window. This can now be turned
off, if you wish (Edit > System preferences... > Windows > When adding
videos/channels/playlists, copy URLs from the system clipboard)
- When adding channels/playlists, you can set the dialogue window to stay open,
which makes adding multiple channels/playlists quicker (Edit >
System preferences... > Windows > When adding channels/playlists, keep the
dialogue window open)
- When creating channels/playlists/folders inside an existing parent folder, a
dialogue window which stays open can be told to continuously re-use that
parent folder (Edit > System preferences... > Windows > When adding
channels/playlists, re-use the optional parent folder)
- When checking/downloading videos, the Results List (the bottom half of the
Progress Tab) can now display videos in reverse order, so you don't have to
scroll down to see the video that was just checked/downloaded (Edit >
System Preferences... > Windows > Show results in reverse order)
- The number of ystem error and warning messages displayed in their own tab
is visible in the tab's label. The label is usually reset when the tab
is made visible. You can now disable this behaviour, preserving the numbers
until the 'Clear the list' button is explicitly clicked (Edit >
System preferences... > Windows > Don't remove number of system messages
from tab label until 'Clear' button is clicked)
- Items in the Video Index (on the left-hand side of the Videos Tab) are sorted
alphabetically. The sorting algorithm has been improved to take account of
numbered items, such that '1 Music' will now appear before '11 Comedy'
- For the benefit of package maintainers (such as a Debian package), Tartube
now uses an environment variable which will prevent Tartube from updating
the youtube-dl binary, if specified. See the comments in setup.py
MAJOR FIXES
- Users whose system Gtk is earlier than v3.24 (this includes many current
Linux distros, but the MS Windows installer) will have experienced graphics
issues, and endless error messages in the terminal window, if open. If your
system Gtk is earlier than v3.24, Tartube will no longer update the Video
Index during a download operation; this should fix the issue at the cost of
disabling real-time updates of the number of videos in each channel,
playlist and folder. Your system's Gtk version is now visible in Tartube's
System Preferences window
- After loading the config file, the download limits were set, but not
displayed in the Progress Tab. Fixed
- Rarely, Tartube crashes (or freezes) when loading a video's JSON metadata
file from your filesystem (but now when downloading it). This should no
longer happen
MINOR FIXES
- The 'Channel properties', 'Folder properties' (etc) windows used the wrong
icon (displaying a folder in the wrong colour). Fixed
- Fixed a 'list modified during sort' error during a download operation
v1.0.0 (31 Jul 2019)
-------------------------------------------------------------------------------
- First beta release
- Fixed some issues with the MS Windows installer
- Some parts of the Tartube window displayed the wrong icons. Fixed
v0.7.0 (7 Jul 2019)
-------------------------------------------------------------------------------
- This is the first release candidate for v1.0.0
- The MS Windows installer has been redesigned again (thanks to slartie for his
generous assistance in getting it working). Some MS Windows 10 users were
still complaining that Tartube would not run; this should be fixed now
- MS Windows should find that the annoying terminal window is no longer visible
- Deleting individual videos, then adding them to the Tartube database again,
could cause problems with the statistics displayed in the Video Index (e.g.
'All Videos (0, -1)'. Fixed
- Deleting and individual video by right-clicking it removed the video from the
database, but didn't delete the video file itself. Fixed
- If an empty channel/playlist was selected, new videos did not automatically
appear in the Video Catalogue during a download operation. Fixed
- Fixed some issues with the Temporary Videos folder
- Fixed more issues with videos being dislayed in the wrong order in the Video
Catalogue
- Fixed more issues with the scrollbars not resetting themselves when switching
between channels/playlists/folders
- After right-clicking a folder, selecting Mark Videos > New didn't work. Fixed
- When marking videos as new/favourite, you can now do this to all videos
in a folder, including all child channels/playlists/folders, or you can do
it just to the videos actually inside that folder. There are several new
popup menu options for emptying a folder, or for removing all its videos
- Tartube will no longer issue a system error if you drag a folder onto itself
in the Video Index
- Fixed the remaining issues caused by the Gtk graphics libraries
- Confirmed that various Gtk issues present in Gtk3.22 are not present in
Gtk 3.24. Users with Gtk 3.22 on their system will be warned to update it.
These warnings can be disabled, if required
v0.6.0 (4 Jul 2019)
-------------------------------------------------------------------------------
- Some MS Windows users, especially on Windows 10, report that they can't run
Tartube at all. In an effort to get around this, the installer has been
redesigned. The way Tartube communicates with youtube-dl on MS Windows has
been changed. youtube-dl update operations should now work flawlessly.
Please report any further problems at our GitHub page; this might be a fix
to issue #10
- Users on Linux/*BSD can now run Tartube directly from the command line, after
installing it (see the README)
- Occasionally, videos were downloaded (or checked) successfully, but Tartube
failed to notice them. This issue should now be fixed
- When checking videos/channels/playlists/folders, only new videos will now
appear in the Results List (in the 'Progress' Tab)
- The toolbar has been redesigned. MS Windows users won't see labels at all
(so everything should fit). Users on all system can turn labels on or off.
Tooltips have been added to the buttons, in case the labels are turned off
- Fixed yet another problem with button to switch the location of Tartube's
data folder (#6)
- Tartube no longer requires the python 'validators' module
- Tartube can now ignore YouTube copyright messages, and also 'Child process
exited with non-zero code' messages, meaning that they won't appear in
Tartube's Errors/Warnings Tab. (They are not ignored by default)
- Tartube now applies a 60-second timeout when youtube-dl tries to download
a video's metadata (since youtube-dl uses a 10-minute timeout); this can be
turned off, if required (#9)
- Fixed some more issues with the way videos are sorted in the Video Catalogue
(it's still not 100%)
- Users can no longer type in comboboxes
- Tartube now spots when the user adds a channel or playlist URL as a video;
only the first video in the channel/playlist is now downloaded
- You can no longer remove download options from a media data object when the
download options edit window is still open
- When you update youtube-dl, Tartube will now tell you which youtube-dl
version is installed. Tartube will no longer claim the update operation
fails if you're using pip (rather than pip3)
v0.5.0 (1 Jul 2019)
-------------------------------------------------------------------------------
- On MS Windows, the fix from v0.4.0 to prevent a crash whenever the user tries
to change the location of Tartube's data directory, did not work. Fixed it
again (#6), and added some dialogue window to make it clearer to the user
what is going on
- Fixed problems with dragging-and-dropping (or otherwise moving) a channel,
playlist or folder to a new location (such as another folder). The videos
were not updated with their new location. The new code will fix any
problems in the Tartube database
- Fixed numerous problems with the code that sorts videos into the right order,
and which displays videos in the Video Catalogue in the right order
- The 'Switch' button no longer resets the page back to the first one
- When switching between channels/playlists/folders, scrollbars are
automatically moved back to the top
- In the toolbar beneath the Video Catalogue, there are two new buttons for
scrolling to the top or bottom of the visible page. Users can now select
a different page just by typing the page number and pressing RETURN. The
same applies to the page size - there is no button to click any more, just
type the new size and press RETURN
- Fixed several problems which were still preventing selection of different
video formats (#3)
- Added bare-bones aac, m4a, mp3, ogg and wav as recognised video formats
- A set of download options can now be completely reset to their default values
- The 'Add Videos' dialogue window, and some others, don't behave well when the
user resizes them. Fixed (#4)
- Added a small folder icon to the the 'Add Videos' dialogue window, and
others, so the user is less likely to forget to set a custom location
- In the 'Add Videos' dialogue window, and others, URLs are now tidied up a
bit because being copied from the clipboard, eliminating leading/trailing
whitespace and empty lines
- The Gtk test window, available for MS Windows after using the installer,
now contains some text (to make it clear that the window is working as
intended)
v0.4.0 (29 Jun 2019)
------------------------------------------------------------------------------
- Drastic improvements to overall performance. Download operations are now much
smoother. You should notice a much lighter burden on your machine's CPU
- The graphics libraries struggled to draw lists containing hundreds (or
thousands!) of videos, so the Video Catalogue has been split into pages. It
typically takes less then a second to show the 'All Videos' folder, if it
contains hundreds of videos, rather than several minutes
- If you just want to find new videos, you can now tell Tartube to stop
checking/downloading channels/playlists as soon as notifications of videos
you've already checked/downloaded start arriving. This works well on
YouTube, which sends the newest videos first, but might not work well on
all websites. The new functionality is turned off by default. Click
'Edit > System preferences > Performance > Time-saving preferences' to turn
it on
- The installer for 32-bit MS Windows failed under all circumstances. Applied a
fix
- On MS Windows, the uninstaller was invisible. It can now be executed from the
Start Menu
- On MS Windows, fixed a crash whenever the user tried to changed the location
of Tartube's data directory (#6)
- Fixed some problems with the 'Add videos' dialogue window, which tried to add
all sorts of invalid URLs (such as lines containing only empty space). The
other 'Add' dialogue windows were also fixed
- A limit to the length of a channel/playlist/folder name now applies
- Leading/trailing whitespace is now removed from URLs and channel/playlist/
folder names
- The 'Add videos' dialogue window now checks for duplicate URLs, and when
found, doesn't add them to the database
- Fixed various problems caused by deleting a video/channel/playlist/folder
(such as the numbers visible in the Video Index). If you've been using an
earlier version of Tartube, any such problems with the database will be
automatically repaired for you (#8)
- Videos which haven't actually been downloaded can now be deleted from the
database by right-clicking them
- Added new options for making backups of the Tartube database file, in case it
becomes corrupted. See the settings in
'Edit > System preferences... > Backups'
- The 'Switch' button now switches between four 'skins' (instead of the
previous two). Two of them show the name of a video's parent channel/
playlist/folder; the other two show the video's description
- Fixed incorrect formatting in the DASH and 3D file formats (#3)
v0.3.0 (25 Jun 2019)
-------------------------------------------------------------------------------
- Tartube will now run on MS Windows
- Fixed some more crashes
- Fixed some issues with video descriptions containing quotes and ampersand
characters
v0.2.0 (23 Jun 2019)
-------------------------------------------------------------------------------
- Corrected old Python2 code to work on Python3
- Greatly expanded the README file
- Fixed the constant crashes
- Fixed some Gtk problems (but others remain unfixed)
- Fixed downloads for users who haven't installed Ffmpeg, and for sites that
don't support Ffmpeg
- Several other minor tweaks/fixes
v0.1.0 (27 May 2019)
-------------------------------------------------------------------------------
- This is the first public release of Tartube

View File

@ -3,5 +3,4 @@ recursive-include docs *
recursive-include icons * recursive-include icons *
recursive-include pack * recursive-include pack *
recursive-include screenshots * recursive-include screenshots *
recursive-include share *

File diff suppressed because it is too large Load Diff

View File

@ -1 +1 @@
name = "tartube" name = "gymbob"

View File

@ -1 +1 @@
#Tartube #GymBob

758
gymbob/editwin.py Normal file
View File

@ -0,0 +1,758 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""Edit window classes."""
# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, GdkPixbuf
# Import other modules
import re
# Import our modules
# ...
# Classes
class ProgEditWin(Gtk.Window):
"""Python class for an 'edit window' to modify values in a gymprog.GymProg
object.
Args:
app_obj (mainapp.TartubeApp): The main application object
edit_obj (gymprog.GymProg): The object whose attributes will be edited
in this window
"""
def __init__(self, app_obj, edit_obj):
Gtk.Window.__init__(self, title='Edit \'' + edit_obj.name + '\'')
# IV list - class objects
# -----------------------
# The mainapp.GymBobApp object
self.app_obj = app_obj
# The gymprog.GymProg object being edited
self.edit_obj = edit_obj
# IV list - Gtk widgets
# ---------------------
self.main_grid = None # Gtk.Grid
self.reset_button = None # Gtk.Button
self.apply_button = None # Gtk.Button
self.ok_button = None # Gtk.Button
self.cancel_button = None # Gtk.Button
self.treeview = None # Gtk.TreeView
self.liststore = None # Gtk.ListStore
# IV list - other
# ---------------
# Size (in pixels) of gaps between edit window widgets
self.spacing_size = self.app_obj.default_spacing_size
# When the user changes a value, it is not applied to self.edit_obj
# immediately; instead, it is stored temporarily in this dictionary
# If the user clicks the 'OK' or 'Apply' buttons at the bottom of the
# window, the changes are applied to self.edit_obj
# If the user clicks the 'Reset' or 'Cancel' buttons, the dictionary
# is emptied and the changes are lost
# The key-value pairs in the dictionary correspond directly to the
# names of attributes, and their values in self.edit_obj
# Key-value pairs are added to this dictionary whenever the user makes
# a change (so if no changes are made when the window is closed, the
# dictionary will still be empty)
self.edit_dict = {}
# Code
# ----
# Set up the edit window
self.setup()
# Public class methods
def setup(self):
"""Called by self.__init__().
Sets up the edit window when it opens.
"""
# Set the default window size
self.set_default_size(
self.app_obj.edit_win_width,
self.app_obj.edit_win_height,
)
# Set the window's Gtk icon list
self.set_icon_list(self.app_obj.main_win_obj.icon_pixbuf_list)
# Set up the window's containing box
self.main_grid = Gtk.Grid()
self.add(self.main_grid)
# Set up main widgets
self.setup_tab()
self.setup_button_strip()
self.setup_gap()
# Procedure complete
self.show_all()
# Inform the main window of this window's birth (so that GymBox doesn't
# the clock to start until all configuration windows have closed)
self.app_obj.main_win_obj.add_child_window(self)
# Add a callback so we can inform the main window of this window's
# destruction
self.connect('destroy', self.close)
def setup_tab(self):
"""Called by self.setup().
Sets up all widgets (except the button strip at the bottom of the
window).
"""
mini_grid = Gtk.Grid()
self.main_grid.attach(mini_grid, 0, 0, 1, 1)
mini_grid.set_border_width(self.spacing_size)
mini_grid.set_column_spacing(self.spacing_size)
mini_grid.set_row_spacing(self.spacing_size)
# Add a treeview
frame = Gtk.Frame()
mini_grid.attach(frame, 0, 0, 5, 1)
frame.set_hexpand(True)
frame.set_vexpand(True)
scrolled = Gtk.ScrolledWindow()
frame.add(scrolled)
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
self.treeview = Gtk.TreeView()
scrolled.add(self.treeview)
self.treeview.set_headers_visible(True)
for i, column_title in enumerate( ['#', 'Time', 'Message', 'Sound'] ):
renderer_text = Gtk.CellRendererText()
column_text = Gtk.TreeViewColumn(
column_title,
renderer_text,
text=i,
)
self.treeview.append_column(column_text)
self.treeview_reset()
self.treeview_refill()
# Add editing widgets beneath the treeview
label = Gtk.Label('Time (in seconds)')
mini_grid.attach(label, 0, 1, 1, 1)
entry = Gtk.Entry()
mini_grid.attach(entry, 1, 1, 2, 1)
entry.set_hexpand(True)
entry.set_max_width_chars(4)
label2 = Gtk.Label('Message')
mini_grid.attach(label2, 0, 2, 1, 1)
entry2 = Gtk.Entry()
mini_grid.attach(entry2, 1, 2, 4, 1)
entry2.set_hexpand(True)
label3 = Gtk.Label('Sound (optional)')
mini_grid.attach(label3, 0, 3, 1, 1)
combostore = Gtk.ListStore(str)
combostore.append('')
for sound_file in self.app_obj.sound_list:
combostore.append( [sound_file] )
combo = Gtk.ComboBox.new_with_model(combostore)
mini_grid.attach(combo, 1, 3, 2, 1)
renderer_text = Gtk.CellRendererText()
combo.pack_start(renderer_text, True)
combo.add_attribute(renderer_text, 'text', 0)
combo.set_entry_text_column(0)
combo.set_active(0)
button = Gtk.Button('Add message')
mini_grid.attach(button, 0, 4, 1, 1)
button.connect(
'clicked',
self.on_button_add_clicked,
entry,
entry2,
combo,
)
button2 = Gtk.Button('Update message')
mini_grid.attach(button2, 1, 4, 1, 1)
button2.connect(
'clicked',
self.on_button_update_clicked,
entry,
entry2,
combo,
)
button3 = Gtk.Button('Delete message')
mini_grid.attach(button3, 2, 4, 1, 1)
button3.connect('clicked', self.on_button_delete_clicked)
button4 = Gtk.Button('Move up')
mini_grid.attach(button4, 3, 4, 1, 1)
button4.connect('clicked', self.on_button_move_up_clicked)
button5 = Gtk.Button('Move down')
mini_grid.attach(button5, 4, 4, 1, 1)
button5.connect('clicked', self.on_button_move_down_clicked)
def setup_button_strip(self):
"""Called by self.setup().
Creates a strip of buttons at the bottom of the window. Any changes the
user has made are applied by clicking the 'OK' or 'Apply' buttons, and
cancelled by using the 'Reset' or 'Cancel' buttons.
The window is closed by using the 'OK' and 'Cancel' buttons.
"""
hbox = Gtk.HBox()
self.main_grid.attach(hbox, 0, 1, 1, 1)
# 'Reset' button
self.reset_button = Gtk.Button('Reset')
hbox.pack_start(self.reset_button, False, False, self.spacing_size)
self.reset_button.get_child().set_width_chars(10)
self.reset_button.set_tooltip_text(
'Reset changes without closing the window',
);
self.reset_button.connect('clicked', self.on_button_reset_clicked)
# 'Apply' button
self.apply_button = Gtk.Button('Apply')
hbox.pack_start(self.apply_button, False, False, self.spacing_size)
self.apply_button.get_child().set_width_chars(10)
self.apply_button.set_tooltip_text(
'Apply changes without closing the window',
);
self.apply_button.connect('clicked', self.on_button_apply_clicked)
# 'OK' button
self.ok_button = Gtk.Button('OK')
hbox.pack_end(self.ok_button, False, False, self.spacing_size)
self.ok_button.get_child().set_width_chars(10)
self.ok_button.set_tooltip_text('Apply changes');
self.ok_button.connect('clicked', self.on_button_ok_clicked)
# 'Cancel' button
self.cancel_button = Gtk.Button('Cancel')
hbox.pack_end(self.cancel_button, False, False, self.spacing_size)
self.cancel_button.get_child().set_width_chars(10)
self.cancel_button.set_tooltip_text('Cancel changes');
self.cancel_button.connect(
'clicked',
self.on_button_cancel_clicked,
)
def setup_gap(self):
"""Called by self.setup().
Adds an empty box beneath the button strip for aesthetic purposes.
"""
hbox = Gtk.HBox()
self.main_grid.attach(hbox, 0, 2, 1, 1)
hbox.set_border_width(self.spacing_size)
def retrieve_val(self, name):
"""Can be called by anything.
Any changes the user has made are temporarily stored in self.edit_dict.
Each key corresponds to an attribute in the object being edited,
self.edit_obj.
If 'name' exists as a key in that dictionary, retrieve the
corresponding value and return it. Otherwise, the user hasn't yet
modified the value, so retrieve directly from the attribute in the
object being edited.
Args:
name (str): The name of the attribute in the object being edited
Returns:
The original or modified value of that attribute.
"""
if name in self.edit_dict:
return self.edit_dict[name]
else:
attrib = getattr(self.edit_obj, name)
return attrib.copy()
def treeview_reset(self):
"""Called by self.setup_tab() and several callback functions.
Creates a model for the Gtk.TreeView to use, replacing any previous
model in use.
"""
self.liststore = Gtk.ListStore(int, int, str, str)
self.treeview.set_model(self.liststore)
def treeview_refill(self):
"""Called by self.setup_tab() and several callback functions, usually
after a call to self.treeview_reset().
Fills the Gtk.Treeview with data.
"""
msg_group_list = self.retrieve_val('msg_group_list')
count = 0
for mini_list in msg_group_list:
count += 1
mod_list = mini_list.copy()
mod_list.insert(0, count)
self.liststore.append(mod_list)
def check_entries(self, entry, entry2):
"""Called by self.on_button_add_clicked() and
.on_button_update_clicked().
Extracts the value of the two Gtk.Entrys. If either value is invalid,
displays an error message.
Args:
entry, entry2 (Gtk.Entry): The entry boxes to check
Return values:
time (int), msg (str): The contents of the boxes, or (None, None)
if either value is invalid
"""
time = entry.get_text()
msg = entry2.get_text()
if time == '' or not re.search('^\d+$', time):
msg_dialogue_win = Gtk.MessageDialog(
self,
0,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
'Invalid time value (must\nbe an integer in seconds)',
)
msg_dialogue_win.run()
msg_dialogue_win.destroy()
return None, None
# elif msg == '':
#
# msg_dialogue_win = Gtk.MessageDialog(
# self,
# 0,
# Gtk.MessageType.ERROR,
# Gtk.ButtonsType.OK,
# 'Invalid message (must\ncontain some characters)',
# )
# msg_dialogue_win.run()
# msg_dialogue_win.destroy()
# return None, None
else:
return time, msg
def apply_changes(self):
"""Called by self.on_button_ok_clicked() and
self.on_button_apply_clicked().
Any changes the user has made are temporarily stored in self.edit_dict.
Apply to those changes to the object being edited.
"""
# Apply any changes the user has made
for key in self.edit_dict.keys():
setattr(self.edit_obj, key, self.edit_dict[key])
# The changes can now be cleared
self.edit_dict = {}
# Save the programme file
self.app_obj.save_prog(self.edit_obj)
def close(self, also_self):
"""Called from callback in self.setup().
Inform the main window that this window is closing.
Args:
also_self (editwin.ProgEditWin): Another copy of self
"""
self.app_obj.main_win_obj.del_child_window(self)
# (Callbacks)
def on_button_add_clicked(self, button, entry, entry2, combo):
"""Called from a callback in self.setup_tab().
Adds a message to the treeview.
Args:
button (Gtk.Button): The widget clicked
entry, entry2 (Gtk.Entry): The contents of these entry boxes are
aded to the treeview (and the gymprog.GymProg programme)
combo (Gtk.ComboBox): The contents of this combobox is added to the
treeview
"""
time, msg = self.check_entries(entry, entry2)
tree_iter = combo.get_active_iter()
model = combo.get_model()
filename = model[tree_iter][0]
if time is not None:
msg_group_list = self.retrieve_val('msg_group_list')
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
mini_list = [int(time), str(msg), str(filename)]
msg_group_list.append(mini_list)
self.edit_dict['msg_group_list'] = msg_group_list
mini_list2 = [
len(msg_group_list), int(time), str(msg), str(filename),
]
self.liststore.append(mini_list2)
def on_button_apply_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Applies any changes made by the user, but doesn't close the window.
Args:
button (Gtk.Button): The widget clicked
"""
# Apply any changes the user has made
self.apply_changes()
def on_button_cancel_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Destroys any changes made by the user and closes the window.
Args:
button (Gtk.Button): The widget clicked
"""
# Destroy the window
self.destroy()
def on_button_delete_clicked(self, button):
"""Called from a callback in self.setup_tab().
Deletes a message from the treeview.
Args:
button (Gtk.Button): The widget clicked
"""
selection = self.treeview.get_selection()
(model, iter) = selection.get_selected()
if iter is None:
# Nothing selected
return
row_num = model[iter][0]
msg_group_list = self.retrieve_val('msg_group_list')
count = 0
mod_list = []
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
for mini_list in msg_group_list:
count += 1
if count != row_num:
mod_list.append(mini_list)
self.edit_dict['msg_group_list'] = mod_list
self.treeview_reset()
self.treeview_refill()
def on_button_ok_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Destroys any changes made by the user and then closes the window.
Args:
button (Gtk.Button): The widget clicked
"""
# Apply any changes the user has made
self.apply_changes()
# Destroy the window
self.destroy()
def on_button_move_down_clicked(self, button):
"""Called from a callback in self.setup_tab().
Moves a message one place down in the treeview.
Args:
button (Gtk.Button): The widget clicked
"""
selection = self.treeview.get_selection()
(model, iter) = selection.get_selected()
if iter is None:
# Nothing selected
return
row_num = model[iter][0]
msg_group_list = self.retrieve_val('msg_group_list')
if row_num < len(msg_group_list):
count = 0
mod_list = []
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
for mini_list in msg_group_list:
count += 1
if count != row_num:
mod_list.append(mini_list)
else:
insert_list = mini_list.copy()
mod_list.insert(row_num, insert_list)
self.edit_dict['msg_group_list'] = mod_list
self.treeview_reset()
self.treeview_refill()
selection = self.treeview.get_selection()
selection.select_path(row_num)
def on_button_move_up_clicked(self, button):
"""Called from a callback in self.setup_tab().
Moves a message one place up in the treeview.
Args:
button (Gtk.Button): The widget clicked
"""
selection = self.treeview.get_selection()
(model, iter) = selection.get_selected()
if iter is None:
# Nothing selected
return
row_num = model[iter][0]
msg_group_list = self.retrieve_val('msg_group_list')
if row_num > 1:
count = 0
mod_list = []
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
for mini_list in msg_group_list:
count += 1
if count != row_num:
mod_list.append(mini_list)
else:
insert_list = mini_list.copy()
mod_list.insert(row_num - 2, insert_list)
self.edit_dict['msg_group_list'] = mod_list
self.treeview_reset()
self.treeview_refill()
selection = self.treeview.get_selection()
selection.select_path(row_num - 2)
def on_button_reset_clicked(self, button):
"""Called from a callback in self.setup_button_strip().
Destroys any changes made by the user and updates the window, showing
self.edit_obj's original values
Args:
button (Gtk.Button): The widget clicked
"""
# Empty self.edit_dict, destroying any changes the user has made
self.edit_dict = {}
# Reset the treeview
self.treeview_reset()
self.treeview_refill()
# Render the changes
self.show_all()
def on_button_update_clicked(self, button, entry, entry2, combo):
"""Called from a callback in self.setup_tab().
Updates the selected message in the treeview.
Args:
button (Gtk.Button): The widget clicked
entry, entry2 (Gtk.Entry): The contents of these entry boxes are
aded to the treeview, updating the selected line (and the
gymprog.GymProg programme)
combo (Gtk.ComboBox): The contents of this combobox is added to the
treeview
"""
selection = self.treeview.get_selection()
(model, iter) = selection.get_selected()
if iter is None:
# Nothing selected
return
row_num = model[iter][0]
time, msg = self.check_entries(entry, entry2)
tree_iter = combo.get_active_iter()
model = combo.get_model()
filename = model[tree_iter][0]
if time is not None:
msg_group_list = self.retrieve_val('msg_group_list')
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
mini_list = [int(time), str(msg), str(filename)]
msg_group_list[row_num - 1] = mini_list
self.edit_dict['msg_group_list'] = msg_group_list
self.treeview_reset()
self.treeview_refill()

35
tartube/tartube → gymbob/gymbob Executable file → Normal file
View File

@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
# Copyright (C) 2019-2020 A S Lewis # Copyright (C) 2020 A S Lewis
# #
# This program is free software: you can redistribute it and/or modify it under # This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software # the terms of the GNU General Public License as published by the Free Software
@ -17,7 +17,7 @@
# this program. If not, see <http://www.gnu.org/licenses/>. # this program. If not, see <http://www.gnu.org/licenses/>.
"""Tartube main file.""" """Gymbob main file."""
# Import Gtk modules # Import Gtk modules
@ -31,7 +31,7 @@ import importlib.util
# Add module directory to path to prevent import issues # Add module directory to path to prevent import issues
spec = importlib.util.find_spec('tartube') spec = importlib.util.find_spec('gymbob')
if spec is not None: if spec is not None:
sys.path.append(os.path.abspath(os.path.dirname(spec.origin))) sys.path.append(os.path.abspath(os.path.dirname(spec.origin)))
@ -41,13 +41,13 @@ import mainapp
# 'Global' variables # 'Global' variables
__packagename__ = 'tartube' __packagename__ = 'gymbob'
__prettyname__ = 'Tartube' __prettyname__ = 'GymBob'
__version__ = '2.0.006' __version__ = '1.002'
__date__ = '3 Mar 2020' __date__ = '28 Mar 2020'
__copyright__ = 'Copyright \xa9 2019-2020 A S Lewis' __copyright__ = 'Copyright \xa9 2020 A S Lewis'
__license__ = """ __license__ = """
Copyright \xa9 2019-2020 A S Lewis. Copyright \xa9 2020 A S Lewis.
This program is free software: you can redistribute it and/or modify it under This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software the terms of the GNU General Public License as published by the Free Software
@ -64,18 +64,11 @@ this program. If not, see <http://www.gnu.org/licenses/>.
__author_list__ = [ __author_list__ = [
'A S Lewis', 'A S Lewis',
] ]
__description__ = 'A front-end GUI for youtube-dl,\n' \ __description__ = 'Simple script to prompt the user during a workout'
+ 'partly based on youtube-dl-gui\n' \ __website__ = 'http://gymbob.sourceforge.io'
+ 'and written in Python 3 / Gtk 3' __app_id__ = 'io.sourceforge.gymbob'
__website__ = 'http://tartube.sourceforge.io'
__app_id__ = 'io.sourceforge.tartube'
# There are three executables; this default one, and two others used in Debian/
# RPM packaging. The others are identical, except for the values of these
# variables
__pkg_install_flag__ = False
__pkg_strict_install_flag__ = False
# Start Tartube # Start Gymbob
app = mainapp.TartubeApp() app = mainapp.GymBobApp()
app.run(sys.argv) app.run(sys.argv)

75
gymbob/gymprog.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""Workout programme classes."""
# Import Gtk modules
# ...
# Import other modules
# ...
# Import our modules
# ...
# Classes
class GymProg(object):
"""Can be called by anything.
Python class that handles a workout programme.
Args:
name (str): A unique programme name
msg_group_list (list): List of messages to display in the main window,
in the form described below
"""
# Standard class methods
def __init__(self, name, msg_group_list=[]):
# IV list - other
# ---------------
# A unique programme name
self.name = name
# The programme is made up of a sequence of messages, displayed after
# fixed intervals
# A message group is a list in the form [int, txt, txt]:
# int: Time in seconds between this message and the previous one
# txt: The message itself (or an empty string to overwrite the
# previous message, leaving no message visible)
# txt: Name of the sound file (.wav or .ogg) in the ../sounds
# folder to play when this message is displayed (or an empty
# string to play no sound)
# The message groups are stored in this sequential list
self.msg_group_list = msg_group_list

901
gymbob/mainapp.py Normal file
View File

@ -0,0 +1,901 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""Main application class."""
# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, GdkPixbuf
# Import other modules
from gi.repository import Gio
import datetime
import json
import os
import playsound
import re
import subprocess
import sys
import threading
import time
# Import our modules
import editwin
import gymprog
import __main__
import mainwin
# Classes
class GymBobApp(Gtk.Application):
"""Main python class for the GymBob application."""
def __init__(self, *args, **kwargs):
super(GymBobApp, self).__init__(
*args,
application_id=None,
flags=Gio.ApplicationFlags.FLAGS_NONE,
**kwargs)
# Instance variable (IV) list - class objects
# -------------------------------------------
# The main window object, set as soon as it's created
self.main_win_obj = None
# Instance variable (IV) list - other
# -----------------------------------
# Default window sizes (in pixels)
self.main_win_width = 550
self.main_win_height = 400
# Default size of edit windows (in pixels)
self.edit_win_width = 600
self.edit_win_height = 400
# Default size (in pixels) of space between various widgets
self.default_spacing_size = 5
# For quick lookup, the directory in which the 'gymbob' executable
# file is found, and its parent directory
self.script_dir = sys.path[0]
self.script_parent_dir = os.path.abspath(
os.path.join(self.script_dir, os.pardir),
)
# The directory in which workout programmes can be stored (as .json
# files)
self.data_dir = os.path.abspath(
os.path.join(
os.path.expanduser('~'),
__main__.__packagename__ + '-data',
),
)
# List of sound files found in the ../sounds directory
self.sound_list = []
# So that a sound can be played from within its own thread, the name of
# the sound file to be played is stored here (temporarily) before
# self.play_sound() can be called
self.sound_file = None
# At all times (after initial setup), a GObject timer runs
# The timer's ID
self.script_timer_id = None
# The timer interval time (in milliseconds)
self.script_timer_time = 100
# Flag set to True if the clock is running
self.clock_running_flag = False
# The time (matches time.time()) at which the user clicked the START
# button
self.clock_start_time = 0
# The time at which the STOP button was clicked. If the START button is
# subsequently clicked, the value of self.clock_start_time is
# adjusted
self.clock_stop_time = 0
# Dictionary of gymprog.GymProg objects, each one handling a single
# workout programme. Dictionary in the form
# prog_dict[unique_name] = GymProg object
# ...where 'unique_name' is a string with a maximum length of 16
self.prog_dict = {}
# The current workout programme object
self.current_prog_obj = None
# Flag set to True when the clock is started (from 0), which starts the
# workout programme
self.current_prog_started_flag = False
# When the clock is started (from 0), the contents of the workout
# programme is copied into this IV
self.current_prog_msg_group_list = []
# The time (matches the clock time, not time.time()) at which the first
# message in self.current_msg_group_list should be displayed. As soon
# as the message is displayed, it is removed from the list
self.current_prog_next_update_time = None
# Flag set to True if sound should has been muted
self.mute_sound_flag = False
def do_startup(self):
"""Gio.Application standard function."""
Gtk.Application.do_startup(self)
# Menu actions
# ------------
# 'GymBob' column
quit_menu_action = Gio.SimpleAction.new('quit_menu', None)
quit_menu_action.connect('activate', self.on_menu_quit)
self.add_action(quit_menu_action)
# 'Programmes' column
new_prog_menu_action = Gio.SimpleAction.new('new_prog_menu', None)
new_prog_menu_action.connect('activate', self.on_menu_new_prog)
self.add_action(new_prog_menu_action)
switch_prog_menu_action = Gio.SimpleAction.new(
'switch_prog_menu',
None,
)
switch_prog_menu_action.connect('activate', self.on_menu_switch_prog)
self.add_action(switch_prog_menu_action)
edit_prog_menu_action = Gio.SimpleAction.new('edit_prog_menu', None)
edit_prog_menu_action.connect('activate', self.on_menu_edit_prog)
self.add_action(edit_prog_menu_action)
delete_prog_menu_action = Gio.SimpleAction.new(
'delete_prog_menu',
None,
)
delete_prog_menu_action.connect('activate', self.on_menu_delete_prog)
self.add_action(delete_prog_menu_action)
# 'Help' column
about_menu_action = Gio.SimpleAction.new('about_menu', None)
about_menu_action.connect('activate', self.on_menu_about)
self.add_action(about_menu_action)
go_website_menu_action = Gio.SimpleAction.new('go_website_menu', None)
go_website_menu_action.connect('activate', self.on_menu_go_website)
self.add_action(go_website_menu_action)
# Button actions
# --------------
start_button_action = Gio.SimpleAction.new('start_button', None)
start_button_action.connect('activate', self.on_button_start)
self.add_action(start_button_action)
stop_button_action = Gio.SimpleAction.new('stop_button', None)
stop_button_action.connect('activate', self.on_button_stop)
self.add_action(stop_button_action)
reset_button_action = Gio.SimpleAction.new('reset_button', None)
reset_button_action.connect('activate', self.on_button_reset)
self.add_action(reset_button_action)
def do_activate(self):
"""Gio.Application standard function."""
self.start()
def do_shutdown(self):
"""Gio.Application standard function."""
# Stop immediately
Gtk.Application.do_shutdown(self)
# Public class methods
def start(self):
"""Called by self.do_activate().
Performs general initialisation.
"""
# Create the main window and make it visible
self.main_win_obj = mainwin.MainWin(self)
self.main_win_obj.show_all()
# Start the GObject timer, which runs continuously, even when the
# visible clock/stopwatch is not running
self.script_timer_id = GObject.timeout_add(
self.script_timer_time,
self.script_timer_callback,
)
# Get a list of available sound files, and sort alphabetically
sound_dir = os.path.abspath(
os.path.join(self.script_parent_dir, 'sounds'),
)
for (dirpath, dir_list, file_list) in os.walk(sound_dir):
for filename in file_list:
if filename != 'COPYING':
self.sound_list.append(filename)
self.sound_list.sort()
# Create the data directory (in which workout programmes are stored as
# .json files), if it doesn't already exist
if not os.path.isdir(self.data_dir):
os.makedirs(self.data_dir)
# Load any workout programmes found in the data directory
for (dirpath, dir_list, file_list) in os.walk(self.data_dir):
# The first file (alphabetically) is the new current workout
# programme
file_list.sort()
for filename in file_list:
if re.search('\.json$', filename):
self.load_prog(filename)
def stop(self):
"""Called by self.on_menu_quit().
Performs a clean shutdown.
"""
# I'm outta here!
self.quit()
def save_prog(self, prog_obj):
"""Called by self.on_menu_new_prog() and
editwin.ProgEditWin.apply_changes().
Saves the specified workout programme as .json file.
Args:
prog_obj (gymprog.GymProg): The workout programme to save
"""
# Prepare the file's data
utc = datetime.datetime.utcfromtimestamp(time.time())
file_path = os.path.abspath(
os.path.join(self.data_dir, prog_obj.name + '.json'),
)
json_dict = {
# Metadata
'script_name': __main__.__packagename__,
'script_version': __main__.__version__,
'save_date': str(utc.strftime('%d %b %Y')),
'save_time': str(utc.strftime('%H:%M:%S')),
# Data
'prog_name': prog_obj.name,
'prog_msg_group_list': prog_obj.msg_group_list,
}
# Try to save the file
try:
with open(file_path, 'w') as outfile:
json.dump(json_dict, outfile, indent=4)
except:
# Save failed
msg_dialogue_win = Gtk.MessageDialog(
self.main_win_obj,
0,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
'Failed to save the \'' + prog_obj.name + '\' programme',
)
msg_dialogue_win.run()
msg_dialogue_win.destroy()
def load_prog(self, filename):
"""Called by self.start().
Loads the named workout programme (a .json file).
Args:
filename (str): The name of the workout programme to load, matching
a key in self.prog_dict, and the name of a file in the data
directory
"""
# Get the full filepath for the specified file
filepath = os.path.abspath(
os.path.join(self.data_dir, filename),
)
# Try to load the file, ignoring any failures
try:
with open(filepath) as infile:
json_dict = json.load(infile)
except:
return
# Do some basic checks on the loaded data
if not json_dict \
or not 'script_name' in json_dict \
or not 'script_version' in json_dict \
or not 'save_date' in json_dict \
or not 'save_time' in json_dict \
or not 'prog_name' in json_dict \
or not 'prog_msg_group_list' in json_dict \
or json_dict['script_name'] != __main__.__packagename__:
return
# Ignore duplicate names (the file name and programme name should be
# the same, but perhaps the user has been editing it by hand...)
if json_dict['prog_name'] in self.prog_dict:
return
# File loaded; create a gymprog.GymProg object for it
prog_obj = gymprog.GymProg(
json_dict['prog_name'],
json_dict['prog_msg_group_list'],
)
self.prog_dict[json_dict['prog_name']] = prog_obj
# The first file loaded is the new current programme. (Files are loaded
# in alphabetical order)
if not self.current_prog_obj:
self.current_prog_obj = prog_obj
self.main_win_obj.update_win_title(prog_obj.name)
self.main_win_obj.update_buttons_on_current_prog()
self.main_win_obj.update_menu_items_on_prog()
def delete_prog(self, prog_obj):
"""Called by self.on_menu_delete_prog().
Deletes the .json file corresponding to the specified workout
programme.
Args:
prog_obj (gymprog.GymProg): The workout programme to delete
"""
file_path = os.path.abspath(
os.path.join(self.data_dir, prog_obj.name + '.json'),
)
# Delete the file
try:
os.remove(file_path)
except:
# Deletion failed
msg_dialogue_win = Gtk.MessageDialog(
self.main_win_obj,
0,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
'Failed to delete the \'' + prog_obj.name + '\' programme',
)
msg_dialogue_win.run()
msg_dialogue_win.destroy()
def play_sound(self):
"""Called periodically by self.script_timer_callback().
If a sound file has been set, play it (unless sound is muted).
"""
sound_file = self.sound_file
self.sound_file = None
# (The value might be None or an empty string)
if not self.mute_sound_flag and sound_file:
path = os.path.abspath(
os.path.join(self.script_parent_dir, 'sounds', sound_file),
)
if os.path.isfile(path):
playsound.playsound(path)
# Callback class methods
# (Timers)
def script_timer_callback(self):
"""Called by GObject timer created by self.start(), ten times a second.
Returns:
1 to keep the timer going, or None to halt it
"""
# (Use the same time value to update all main window widgets)
current_time = time.time()
# If the programme is running...
if self.clock_running_flag:
# Update the contents of textviews in the main window
# (De)sensitise menu items and/or buttons
clock_time = current_time - self.clock_start_time
self.main_win_obj.update_clock_textview(clock_time)
if self.current_prog_next_update_time is not None \
and self.current_prog_next_update_time <= clock_time \
and self.current_prog_msg_group_list:
mini_list = self.current_prog_msg_group_list.pop(0)
self.main_win_obj.update_this_info_textview(str(mini_list[1]))
if mini_list[2]:
self.sound_file = mini_list[2]
if self.current_prog_msg_group_list:
# Programme not finished; show the next message as well (in
# a different colour)
next_mini_list = self.current_prog_msg_group_list[0]
self.current_prog_next_update_time += next_mini_list[0]
self.main_win_obj.update_next_info_textview(
next_mini_list[1],
)
else:
# Programme finished
self.current_prog_next_update_time = None
self.main_win_obj.update_countdown_textview('')
self.main_win_obj.update_next_info_textview('')
if self.current_prog_next_update_time is not None:
next_time = self.current_prog_next_update_time - clock_time
self.main_win_obj.update_countdown_textview(next_time)
# Play a sound file, if required. In case of long sounds, perform this
# action inside its own thread
if self.sound_file is not None:
thread = threading.Thread(target=self.play_sound)
thread.start()
# Return 1 to keep the timer going
return 1
# (Widgets)
def on_button_start(self, action, par):
"""Called from a callback in self.do_startup().
Starts the clock.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Update IVs
self.clock_running_flag = True
if not self.clock_start_time:
self.clock_start_time = time.time()
else:
self.clock_start_time += (time.time() - self.clock_stop_time)
# (De)sensitise buttons
self.main_win_obj.update_buttons_on_start()
if not self.current_prog_started_flag:
# Clock is at 0; start the workout programme
self.current_prog_started_flag = True
self.current_prog_msg_group_list \
= self.current_prog_obj.msg_group_list.copy()
# (The workout programme might be empty, so we have to check for
# that)
if self.current_prog_msg_group_list:
# mini_list is in the form
# (time_in_seconds, message, optional_sound_file)
mini_list = self.current_prog_msg_group_list[0]
self.current_prog_next_update_time = mini_list[0]
if len(self.current_prog_msg_group_list) > 1:
# Programme not finished; show the next message as well (in
# a different colour)
next_mini_list = self.current_prog_msg_group_list[0]
self.main_win_obj.update_next_info_textview(
next_mini_list[1],
)
# (De)sensitise menu items
self.main_win_obj.update_menu_items_on_prog()
def on_button_stop(self, action, par):
"""Called from a callback in self.do_startup().
Stops the clock.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Update IVs
self.clock_running_flag = False
self.clock_stop_time = time.time()
# (De)sensitise buttons
self.main_win_obj.update_buttons_on_stop()
# (De)sensitise menu items
self.main_win_obj.update_menu_items_on_prog()
def on_button_reset(self, action, par):
"""Called from a callback in self.do_startup().
Resets the clock.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Update IVs
self.clock_running_flag = False
self.clock_start_time = 0
self.clock_stop_time = 0
self.current_prog_started_flag = False
self.current_prog_msg_group_list = []
self.current_prog_next_update_time = None
# Reset textviews
self.main_win_obj.update_clock_textview('')
self.main_win_obj.update_countdown_textview('')
self.main_win_obj.update_this_info_textview('')
self.main_win_obj.update_next_info_textview('')
# (De)sensitise buttons
self.main_win_obj.update_buttons_on_reset()
# (De)sensitise menu items
self.main_win_obj.update_menu_items_on_prog()
def on_menu_about(self, action, par):
"""Called from a callback in self.do_startup().
Show a standard 'about' dialogue window.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
dialogue_win = Gtk.AboutDialog()
dialogue_win.set_transient_for(self.main_win_obj)
dialogue_win.set_destroy_with_parent(True)
dialogue_win.set_program_name(__main__.__packagename__.title())
dialogue_win.set_version('v' + __main__.__version__)
dialogue_win.set_copyright(__main__.__copyright__)
dialogue_win.set_license(__main__.__license__)
dialogue_win.set_website(__main__.__website__)
dialogue_win.set_website_label(
__main__.__prettyname__ + ' website'
)
dialogue_win.set_comments(__main__.__description__)
dialogue_win.set_logo(
self.main_win_obj.icon_pixbuf_dict['icon_64'],
)
dialogue_win.set_authors(__main__.__author_list__)
dialogue_win.set_title('')
dialogue_win.connect('response', self.on_menu_about_close)
dialogue_win.show()
def on_menu_about_close(self, action, par):
"""Called from a callback in self.on_menu_about().
Close the 'about' dialogue window.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
action.destroy()
def on_menu_delete_prog(self, action, par):
"""Called from a callback in self.do_startup().
Prompts the user to delete a workout programme.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Open a dialogue window
dialogue_win = mainwin.DeleteProgDialogue(self.main_win_obj)
response = dialogue_win.run()
# Retrieve user choices from the dialogue window...
prog_name = dialogue_win.prog_name
# ...before destroying the dialogue window
dialogue_win.destroy()
if response == Gtk.ResponseType.OK:
# Remove the workout programme from the registry
del self.prog_dict[prog_name]
# If it's the current programme, update some things
if self.current_prog_obj is not None \
and self.current_prog_obj.name == prog_name:
# Reset the window title
self.main_win_obj.update_win_title()
# Delete the corresponding file
self.delete_prog(self.current_prog_obj)
self.current_prog_obj = None
# Artificially click the RESET button to reset the clock and
# empty the main window of text
self.main_win_obj.reset_button.clicked()
# (De)sensitise buttons
self.main_win_obj.update_buttons_on_current_prog()
# Desensitise menu items, if there are no workout programmes left
self.main_win_obj.update_menu_items_on_prog()
def on_menu_edit_prog(self, action, par):
"""Called from a callback in self.do_startup().
Prompts the user to edit the current workout programme.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Open the edit window immediately
editwin.ProgEditWin(self, self.current_prog_obj)
def on_menu_go_website(self, action, par):
"""Called from a callback in self.do_startup().
Opens the GymBob website.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
uri = __main__.__website__
# Open the GymBob home page in the user's default browser
if sys.platform == "win32":
os.startfile(uri)
else:
opener ="open" if sys.platform == "darwin" else "xdg-open"
subprocess.call([opener, uri])
def on_menu_new_prog(self, action, par):
"""Called from a callback in self.do_startup().
Prompts the user for the name of a new workout programme.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Open a dialogue window
dialogue_win = mainwin.NewProgDialogue(self.main_win_obj)
response = dialogue_win.run()
# Retrieve user choices from the dialogue window...
prog_name = dialogue_win.entry.get_text()
# ...before destroying the dialogue window
dialogue_win.destroy()
if response != Gtk.ResponseType.OK:
return
if prog_name in self.prog_dict:
# Duplicate programme names are not allowed
msg_dialogue_win = Gtk.MessageDialog(
self.main_win_obj,
0,
Gtk.MessageType.ERROR,
Gtk.ButtonsType.OK,
'A programme called \'' + prog_name + '\' already exists!',
)
msg_dialogue_win.run()
msg_dialogue_win.destroy()
return
# Create the new workout programme
prog_obj = gymprog.GymProg(prog_name)
self.prog_dict[prog_name] = prog_obj
# Set it as the current programme
self.current_prog_obj = prog_obj
# Save the empty programme, creating a .json file
self.save_prog(prog_obj)
# Update the main window title to show the current programme
self.main_win_obj.update_win_title(prog_obj.name)
# (De)sensitise menu items and buttons
self.main_win_obj.update_buttons_on_current_prog()
self.main_win_obj.update_menu_items_on_prog()
# Open an edit window for the new programme immediately
editwin.ProgEditWin(self, prog_obj)
def on_menu_switch_prog(self, action, par):
"""Called from a callback in self.do_startup().
Prompts the user with a list of workout programmes from which to
select.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Open a dialogue window
dialogue_win = mainwin.SwitchProgDialogue(self.main_win_obj)
response = dialogue_win.run()
# Retrieve user choices from the dialogue window...
prog_name = dialogue_win.prog_name
# ...before destroying the dialogue window
dialogue_win.destroy()
if response == Gtk.ResponseType.OK:
# Set the current programme
self.current_prog_obj = self.prog_dict[prog_name]
# (De)sensitise menu items and buttons
self.main_win_obj.update_buttons_on_current_prog()
self.main_win_obj.update_menu_items_on_prog()
# Artificially click the RESET button to reset the clock and empty
# the main window of text
self.main_win_obj.reset_button.clicked()
# Update the main window title to show the current programme
self.main_win_obj.update_win_title(self.current_prog_obj.name)
def on_menu_quit(self, action, par):
"""Called from a callback in self.do_startup().
Terminates the GymBob app.
Args:
action (Gio.SimpleAction): Object generated by Gio
par (None): Ignored
"""
# Terminate the script
self.stop()
# Set accessors
def set_mute_sound_flag(self, flag):
if not flag:
self.mute_sound_flag = False
else:
self.mute_sound_flag = True

998
gymbob/mainwin.py Normal file
View File

@ -0,0 +1,998 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (C) 2020 A S Lewis
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
"""Main window class."""
# Import Gtk modules
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject, Gdk, GdkPixbuf
# Import other modules
from gi.repository import Gio
import math
import os
# Import our modules
import __main__
import mainapp
# Classes
class MainWin(Gtk.ApplicationWindow):
"""Called by mainapp.GymBobApp.start().
Python class that handles the main window.
Args:
app_obj (mainapp.TartubeApp): The main application object
"""
def __init__(self, app_obj):
super(MainWin, self).__init__(
title=__main__.__prettyname__,
application=app_obj
)
# IV list - class objects
# -----------------------
# The main application
self.app_obj = app_obj
# IV list - Gtk widgets
# ---------------------
# (from self.setup_grid)
self.grid = None # Gtk.Grid
# (from self.setup_menubar)
self.menubar = None # Gtk.MenuBar
self.new_prog_menu_item = None # Gtk.MenuItem
self.switch_prog_menu_item = None # Gtk.MenuItem
self.edit_prog_menu_item = None # Gtk.MenuItem
self.delete_prog_menu_item = None # Gtk.MenuItem
# (from self.setup_win)
self.clock_textview = None # Gtk.TextView
self.countdown_textview = None # Gtk.TextView
self.this_info_textview = None # Gtk.TextView
self.next_info_textview = None # Gtk.TextView
self.start_button = None # Gtk.Button
self.pause_button = None # Gtk.Button
self.stop_button = None # Gtk.Button
self.reset_button = None # Gtk.Button
# IV list - other
# ---------------
# Colours to use in the upper/lower textviews, and the font size (in
# points, default value is 10)
self.clock_bg_colour = '#000000'
self.clock_text_colour = '#FFFFFF'
self.clock_font_size = 40
self.countdown_bg_colour = '#000000'
self.countdown_text_colour = '#FF0000'
self.countdown_font_size = 40
self.this_info_bg_colour = '#000000'
self.this_info_text_colour = '#FFFFFF'
self.this_info_font_size = 30
self.next_info_bg_colour = '#000000'
self.next_info_text_colour = '#FF0000'
self.next_info_font_size = 30
# Gymbob icon files are loaded into pixbufs, ready for use. Dictionary
# in the form:
# key - a string like 'icon_512'
# value - a Gdk.Pixbuf
self.icon_pixbuf_dict = {}
# The same list of pixbufs in sequential order
self.icon_pixbuf_list = []
# List of edit windows (editwin.ProgEditWin objects) that are currently
# open. The clock can't be started if any edit windows are open
self.edit_win_list = []
# Code
# ----
# Set up icon pixbufs
self.setup_icons()
# Set up the main window
self.setup_win()
# Public class methods
def setup_icons(self):
"""Called by self.__init__().
Sets up pixbufs for GymBob icons.
"""
# The default location for icons is ../icons
# When installed via PyPI, the icons are moved to ../gymbob/icons
# When installed via a Debian/RPM package, the icons are moved to
# /usr/share/gymbob/icons
icon_dir_list = []
icon_dir_list.append(
os.path.abspath(
os.path.join(self.app_obj.script_parent_dir, 'icons'),
),
)
icon_dir_list.append(
os.path.abspath(
os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'icons',
),
),
)
icon_dir_list.append(
os.path.join(
'/', 'usr', 'share', __main__.__packagename__, 'icons',
)
)
self.icon_pixbuf_list = []
for icon_dir_path in icon_dir_list:
if os.path.isdir(icon_dir_path):
for size in [16, 24, 32, 48, 64, 128, 256, 512]:
path = os.path.abspath(
os.path.join(
icon_dir_path,
'win',
__main__.__packagename__ + '_icon_' + str(size) \
+ '.png',
),
)
pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
self.icon_pixbuf_list.append(pixbuf)
self.icon_pixbuf_dict['icon_' + str(size)] = pixbuf
# Pass the list of pixbufs to Gtk
self.set_icon_list(self.icon_pixbuf_list)
def setup_win(self):
"""Called by self.__init__().
Sets up the main window, calling various function to create its
widgets.
"""
spacing = self.app_obj.default_spacing_size
# Set the default window size
self.set_default_size(
self.app_obj.main_win_width,
self.app_obj.main_win_height,
)
# Create main window widgets
self.grid = Gtk.Grid()
self.add(self.grid)
self.setup_menubar()
self.clock_textview = self.setup_textview(
1,
self.clock_bg_colour,
self.clock_text_colour,
self.clock_font_size,
0, 1, 1, 1,
)
self.countdown_textview = self.setup_textview(
2,
self.countdown_bg_colour,
self.countdown_text_colour,
self.countdown_font_size,
1, 1, 1, 1,
)
self.this_info_textview = self.setup_textview(
3,
self.this_info_bg_colour,
self.this_info_text_colour,
self.this_info_font_size,
0, 2, 2, 1,
)
self.next_info_textview = self.setup_textview(
4,
self.next_info_bg_colour,
self.next_info_text_colour,
self.next_info_font_size,
0, 3, 2, 1,
)
self.setup_dummy_textview()
# Separator
self.grid.attach(Gtk.Separator(), 0, 4, 2, 1)
hbox = Gtk.HBox()
self.grid.attach(hbox, 0, 5, 2, 1)
hbox.set_border_width(spacing * 2)
hbox.set_vexpand(True)
self.start_button = Gtk.Button('START')
hbox.pack_start(self.start_button, True, True, spacing)
self.start_button.set_action_name('app.start_button')
# (These buttons are desensitised until a programme is loaded/created)
self.start_button.set_sensitive(False)
self.stop_button = Gtk.Button('STOP')
hbox.pack_start(self.stop_button, True, True, spacing)
self.stop_button.set_sensitive(False)
self.stop_button.set_action_name('app.stop_button')
self.stop_button.set_sensitive(False)
self.reset_button = Gtk.Button('RESET')
hbox.pack_start(self.reset_button, True, True, spacing)
self.reset_button.set_action_name('app.reset_button')
self.reset_button.set_sensitive(False)
def setup_menubar(self):
"""Called by self.setup_win().
Sets up a Gtk.Menu at the top of the main window.
"""
self.menubar = Gtk.MenuBar()
self.grid.attach(self.menubar, 0, 0, 2, 1)
# GymBob column
file_menu_column = Gtk.MenuItem.new_with_mnemonic(
'_' + __main__.__prettyname__,
)
self.menubar.add(file_menu_column)
file_sub_menu = Gtk.Menu()
file_menu_column.set_submenu(file_sub_menu)
mute_sound_menu_item = Gtk.CheckMenuItem.new_with_mnemonic(
'_Mute sound',
)
mute_sound_menu_item.set_active(self.app_obj.mute_sound_flag)
mute_sound_menu_item.connect(
'activate',
self.on_menu_mute_sound,
)
file_sub_menu.append(mute_sound_menu_item)
# Separator
file_sub_menu.append(Gtk.SeparatorMenuItem())
quit_menu_item = Gtk.MenuItem.new_with_mnemonic('_Quit')
file_sub_menu.append(quit_menu_item)
quit_menu_item.set_action_name('app.quit_menu')
# Programmes column
edit_menu_column = Gtk.MenuItem.new_with_mnemonic('_Programmes')
self.menubar.add(edit_menu_column)
edit_sub_menu = Gtk.Menu()
edit_menu_column.set_submenu(edit_sub_menu)
self.new_prog_menu_item = Gtk.MenuItem.new_with_mnemonic(
'_New programme...',
)
edit_sub_menu.append(self.new_prog_menu_item)
self.new_prog_menu_item.set_action_name('app.new_prog_menu')
self.switch_prog_menu_item = Gtk.MenuItem.new_with_mnemonic(
'_Switch programme...',
)
edit_sub_menu.append(self.switch_prog_menu_item)
self.switch_prog_menu_item.set_action_name('app.switch_prog_menu')
self.edit_prog_menu_item = Gtk.MenuItem.new_with_mnemonic(
'_Edit current programme...',
)
edit_sub_menu.append(self.edit_prog_menu_item)
self.edit_prog_menu_item.set_action_name('app.edit_prog_menu')
# Separator
edit_sub_menu.append(Gtk.SeparatorMenuItem())
self.delete_prog_menu_item = Gtk.MenuItem.new_with_mnemonic(
'_Delete programme...',
)
edit_sub_menu.append(self.delete_prog_menu_item)
self.delete_prog_menu_item.set_action_name('app.delete_prog_menu')
# Help column
help_menu_column = Gtk.MenuItem.new_with_mnemonic('_Help')
self.menubar.add(help_menu_column)
help_sub_menu = Gtk.Menu()
help_menu_column.set_submenu(help_sub_menu)
about_menu_item = Gtk.MenuItem.new_with_mnemonic('_About...')
help_sub_menu.append(about_menu_item)
about_menu_item.set_action_name('app.about_menu')
go_website_menu_item = Gtk.MenuItem.new_with_mnemonic('Go to _website')
help_sub_menu.append(go_website_menu_item)
go_website_menu_item.set_action_name('app.go_website_menu')
# (Some menu items are desensitised until a programme is loaded/
# created)
self.edit_prog_menu_item.set_sensitive(False)
self.switch_prog_menu_item.set_sensitive(False)
self.delete_prog_menu_item.set_sensitive(False)
def setup_textview(self, widget_id, bg_colour, text_colour, font_size, \
x_pos, y_pos, width, height):
"""Called by self.setup_win().
Creates one of the main window textviews (there are four in all).
Args:
widget_id (int): Unique number for this textview (1-4)
bg_colour, text_colour (str): The colours to use in this textview
(e.g. '#FFFFFF')
font_size (int): The font size (in points, e.g. 10)
x_pos, y_pos, width, height (int): Coordinates on the Gtk3.Grid
Return values:
The Gtk.TextView created
"""
# Add a textview to the grid, using a css style sheet to provide (for
# example) monospaced white text on a black background
scrolled = Gtk.ScrolledWindow()
self.grid.attach(scrolled, x_pos, y_pos, width, height)
scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
frame = Gtk.Frame()
scrolled.add_with_viewport(frame)
style_provider = self.set_textview_css(
'#css_text_id_' + str(widget_id) \
+ ', textview text {\n' \
+ ' background-color: ' + bg_colour + ';\n' \
+ ' color: ' + text_colour + ';\n' \
+ '}\n' \
+ '#css_label_id_' + str(widget_id) \
+ ', textview {\n' \
+ ' font-family: monospace, monospace;\n' \
+ ' font-size: ' + str(font_size) + 'pt;\n' \
+ '}'
)
textview = Gtk.TextView()
frame.add(textview)
textview.set_wrap_mode(Gtk.WrapMode.WORD)
textview.set_editable(False)
textview.set_cursor_visible(False)
textview.set_hexpand(True)
textview.set_vexpand(True)
context = textview.get_style_context()
context.add_provider(style_provider, 600)
return textview
def setup_dummy_textview(self):
"""Called by self.setup_win(), immediately after calls to
self.setup_textview().
Resets css properties for the next Gtk.TextView created (presumably by
another application), so it uses the default style, not the css style
specified in the calls to self.setup_textview().
"""
# Create a dummy textview that's not visible in the main window
textview = Gtk.TextView()
style_provider = self.set_textview_css(
'#css_text_id_default, textview text {\n' \
+ ' background-color: unset;\n' \
+ ' color: unset;\n' \
+ '}\n' \
+ '#css_label_id_default, textview {\n' \
+ ' font-family: unset;\n' \
+ ' font-size: unset;\n' \
+ '}'
)
context = textview.get_style_context()
context.add_provider(style_provider, 600)
def set_textview_css(self, css_string):
"""Called by self.setup_upper_textview() and .setup_lower_textview().
Applies a CSS style to the current screen, which is used for the
Gtk.TextView that has just been created.
Called a third time to create a dummy textview with default properties.
Args:
css_string (str): The CSS style to apply
Returns:
The Gtk.CssProvider created
"""
style_provider = Gtk.CssProvider()
style_provider.load_from_data(bytes(css_string.encode()))
Gtk.StyleContext.add_provider_for_screen(
Gdk.Screen.get_default(),
style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
)
return style_provider
# (Update widgets)
def update_win_title(self, prog_name=None):
"""Called by various functions.
Changes the title of the main window. If there is a current programme,
display its name alongside the name of the script.
Args:
prog_name (str): The name of a workout programme, matching a key
in self.app_obj.prog_dict
"""
if prog_name is not None:
self.set_title(__main__.__prettyname__ + ' [' + prog_name + ']')
else:
self.set_title(__main__.__prettyname__)
def update_clock_textview(self, time):
"""Called by various functions.
Updates the main window textview showing the current time.
Args:
time (int): The current time (since the programme begun) in seconds
"""
if not time:
self.clock_textview.get_buffer().set_text('')
else:
self.clock_textview.get_buffer().set_text(
self.convert_time_to_string(time),
)
def update_countdown_textview(self, time):
"""Called by various functions.
Updates the main window textview showing the time until the next
message to be displayed.
Args:
time (int): The time in seconds
"""
if not time:
self.countdown_textview.get_buffer().set_text('')
else:
self.countdown_textview.get_buffer().set_text(
self.convert_time_to_string(math.ceil(time)),
)
def update_this_info_textview(self, msg):
"""Called by various functions.
Updates the main window textview with the current message (if any).
Args:
msg (str): The message to display (use an empty string to clear the
textview)
"""
self.this_info_textview.get_buffer().set_text(str(msg))
def update_next_info_textview(self, msg):
"""Called by various functions.
Updates the main window textview with the next message (if any).
Args:
msg (str): The message to display (use an empty string to clear the
textview)
"""
self.next_info_textview.get_buffer().set_text(str(msg))
def update_menu_items_on_prog(self):
"""Called by various functions.
(De)sensitises menu items depending on whether any workout programmes
exist, or not.
"""
if self.app_obj.current_prog_started_flag:
self.new_prog_menu_item.set_sensitive(False)
else:
self.new_prog_menu_item.set_sensitive(True)
if not self.app_obj.prog_dict:
self.edit_prog_menu_item.set_sensitive(False)
else:
self.edit_prog_menu_item.set_sensitive(True)
if not self.app_obj.prog_dict \
or self.app_obj.current_prog_started_flag:
self.switch_prog_menu_item.set_sensitive(False)
self.delete_prog_menu_item.set_sensitive(False)
else:
self.switch_prog_menu_item.set_sensitive(True)
self.delete_prog_menu_item.set_sensitive(True)
def update_buttons_on_start(self):
"""Called by mainapp.GymBobApp.on_button_start().
(De)sensitises buttons after the user clicks the START button.
"""
self.start_button.set_sensitive(False)
self.stop_button.set_sensitive(True)
self.reset_button.set_sensitive(False)
def update_buttons_on_stop(self):
"""Called by mainapp.GymBobApp.on_button_stop().
(De)sensitises buttons after the user clicks the STOP button.
"""
self.start_button.set_sensitive(True)
self.stop_button.set_sensitive(False)
self.reset_button.set_sensitive(True)
def update_buttons_on_reset(self):
"""Called by mainapp.GymBobApp.on_button_reset().
(De)sensitises buttons after the user clicks the RESET button.
"""
self.start_button.set_sensitive(True)
self.stop_button.set_sensitive(False)
self.reset_button.set_sensitive(True)
def update_buttons_on_current_prog(self):
"""Called by various functions.
(De)sensitises the START, STOP and RESET buttons, depending on whether
there is a current workout programme, or not.
"""
if not self.app_obj.current_prog_obj:
self.start_button.set_sensitive(False)
self.stop_button.set_sensitive(False)
self.reset_button.set_sensitive(False)
else:
self.start_button.set_sensitive(True)
self.stop_button.set_sensitive(True)
self.reset_button.set_sensitive(True)
# (Support functions)
def convert_time_to_string(self, time):
"""Called by various functions.
Converts a time value (an integer in seconds) into a formatted string,
e.g. '1:27:02'.
Args:
time (int): A value in seconds (0 or above)
Return values:
The converted string
"""
minutes = int(time / 60)
seconds = int(time % 60)
hours = int(minutes / 60)
minutes = int(minutes % 60)
time_str = str(minutes).zfill(2) + ':' + str(seconds).zfill(2)
if hours > 0:
time_str = str(hours) + ':' + time_str
return time_str
def add_child_window(self, edit_win_obj):
"""Called by editwin.ProgEditWin.setup().
When an edit window opens, add it to our list of such windows. (A
workout programme will not start while the window(s) are open.)
Args:
edit_win_obj (edit.ProgEditWin): The window to add
"""
# Check that the window isn't already in the list (unlikely, but check
# anyway)
if not edit_win_obj in self.edit_win_list:
# Update the IV
self.edit_win_list.append(edit_win_obj)
def del_child_window(self, edit_win_obj):
"""Called by editwin.ProgEditWin.close().
When an edit window closes, remove it to our list of such windows.
Args:
edit_win_obj (edit.ProgEditWin): The window to remove
"""
# Update the IV
if edit_win_obj in self.edit_win_list:
self.edit_win_list.remove(edit_win_obj)
# Callbacks
def on_menu_mute_sound(self, checkbutton):
"""Called from a callback in self.setup_menubar().
Mutes (or unmutes) sound effects.
Args:
checkbutton (Gtk.CheckButton): The widget clicked.
"""
self.app_obj.set_mute_sound_flag(checkbutton.get_active())
class DeleteProgDialogue(Gtk.Dialog):
"""Called by mainapp.GymBobApp.on_menu_switch_prog().
Python class handling a dialogue window that prompts the user to delete a
workout programme.
Args:
main_win_obj (mainwin.MainWin): The parent main window
"""
def __init__(self, main_win_obj):
# IV list - Gtk widgets
# ---------------------
self.combo = None # Gtk.ComboBox
# IV list - other
# ---------------
self.prog_name = None
self.prog_list = []
# Code
# ----
Gtk.Dialog.__init__(
self,
'Delete programme',
main_win_obj,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK,
)
)
self.set_modal(True)
# Set up the dialogue window
box = self.get_content_area()
grid = Gtk.Grid()
box.add(grid)
grid.set_border_width(main_win_obj.app_obj.default_spacing_size)
grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size)
label = Gtk.Label('Select the programme to delete')
grid.attach(label, 0, 0, 1, 1)
# Import and display a sorted list of workout programmes
self.prog_list = list(main_win_obj.app_obj.prog_dict.keys())
self.prog_list.sort()
self.prog_name = self.prog_list[0]
listmodel = Gtk.ListStore(str)
for item in self.prog_list:
listmodel.append([item])
self.combo = Gtk.ComboBox.new_with_model(listmodel)
grid.attach(self.combo, 0, 1, 1, 1)
self.combo.set_hexpand(True)
cell = Gtk.CellRendererText()
self.combo.pack_start(cell, False)
self.combo.add_attribute(cell, 'text', 0)
self.combo.set_active(0)
self.combo.connect('changed', self.on_combo_changed)
# Display the dialogue window
self.show_all()
def on_combo_changed(self, combo):
"""Called from callback in self.__init__().
Store the combobox's selected item, so the calling function can
retrieve it.
Args:
combo (Gtk.ComboBox): The clicked widget
"""
self.prog_name = self.prog_list[combo.get_active()]
class NewProgDialogue(Gtk.Dialog):
"""Called by mainapp.GymBobApp.on_menu_new_prog().
Python class handling a dialogue window that prompts the user for the name
of a new workout programme.
Args:
main_win_obj (mainwin.MainWin): The parent main window
"""
def __init__(self, main_win_obj):
# IV list - Gtk widgets
# ---------------------
self.entry = None # Gtk.Entry
# Code
# ----
Gtk.Dialog.__init__(
self,
'New programme',
main_win_obj,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK,
)
)
self.set_modal(True)
# Set up the dialogue window
box = self.get_content_area()
grid = Gtk.Grid()
box.add(grid)
grid.set_border_width(main_win_obj.app_obj.default_spacing_size)
grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size)
label = Gtk.Label('Enter the name of a new workout programme')
grid.attach(label, 0, 0, 1, 1)
self.entry = Gtk.Entry()
grid.attach(self.entry, 0, 1, 1, 1)
self.entry.set_hexpand(True)
self.entry.set_max_length(16)
# Display the dialogue window
self.show_all()
class SwitchProgDialogue(Gtk.Dialog):
"""Called by mainapp.GymBobApp.on_menu_switch_prog().
Python class handling a dialogue window that prompts the user to switch to
a new workout programme.
Args:
main_win_obj (mainwin.MainWin): The parent main window
"""
def __init__(self, main_win_obj):
# IV list - Gtk widgets
# ---------------------
self.combo = None # Gtk.ComboBox
# IV list - other
# ---------------
self.prog_name = None
self.prog_list = []
# Code
# ----
Gtk.Dialog.__init__(
self,
'Switch programme',
main_win_obj,
Gtk.DialogFlags.DESTROY_WITH_PARENT,
(
Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
Gtk.STOCK_OK, Gtk.ResponseType.OK,
)
)
self.set_modal(True)
# Set up the dialogue window
box = self.get_content_area()
grid = Gtk.Grid()
box.add(grid)
grid.set_border_width(main_win_obj.app_obj.default_spacing_size)
grid.set_row_spacing(main_win_obj.app_obj.default_spacing_size)
label = Gtk.Label('Set the new workout programme')
grid.attach(label, 0, 0, 1, 1)
# Import a sorted list of programmes. The current programme should be
# the first item in the list
import_list = list(main_win_obj.app_obj.prog_dict.keys())
sorted_list = []
self.prog_name = main_win_obj.app_obj.current_prog_obj.name
for item in import_list:
if item != self.prog_name:
self.prog_list.append(item)
self.prog_list.sort()
self.prog_list.insert(0, self.prog_name)
listmodel = Gtk.ListStore(str)
for item in self.prog_list:
listmodel.append([item])
self.combo = Gtk.ComboBox.new_with_model(listmodel)
grid.attach(self.combo, 0, 1, 1, 1)
self.combo.set_hexpand(True)
cell = Gtk.CellRendererText()
self.combo.pack_start(cell, False)
self.combo.add_attribute(cell, 'text', 0)
self.combo.set_active(0)
self.combo.connect('changed', self.on_combo_changed)
# Display the dialogue window
self.show_all()
def on_combo_changed(self, combo):
"""Called from callback in self.__init__().
Store the combobox's selected item, so the calling function can
retrieve it.
Args:
combo (Gtk.ComboBox): The clicked widget
"""
self.prog_name = self.prog_list[combo.get_active()]

View File

@ -1,39 +1,9 @@
COPYING COPYING
All files in the ../dialogue sub-directory All files in ../win sub-directory
All files in the ../status sub-directory
All files in the ../win sub-directory
Author: Vectorgraphit Author: Carlo Rodriguez
Source: https://www.iconfinder.com/icons/199499/ Source: https://www.iconfinder.com/icons/512534/exercise_fitness_gym_gymnasium_icon
Author: bekeen studio
Source: https://www.iconfinder.com/bekeenstudio
This work is licensed under the Creative Commons Attribution 3.0 Unported "Free for commercial use"
License. To view a copy of this license, visit
http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative
Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
All files in the ../large sub-directory
All files in the ../msg sub-directory
All files in the ../small sub-directory
All files in the ../toolbar sub-directory
Author: FatCow Web Hosting
Source: https://www.fatcow.com/free-icons
This work is licensed under the Creative Commons Attribution 3.0 Generic
License. To view a copy of this license, visit
http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative
Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.
All files in the ../locale directory
Author: Mr. Hopnguyen
Source: https://www.iconfinder.com/icons/2634450/
This work is licensed under the Creative Commons Attribution 3.0 Unported
License. To view a copy of this license, visit
http://creativecommons.org/licenses/by/3.0/ or send a letter to Creative
Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 632 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Some files were not shown because too many files have changed in this diff Show More