Revert "Fix PyPI installation issues"
This reverts commit 24efd5450b5d195eac9ac0e321dfebb0ae087572.
7
AUTHORS
@ -5,5 +5,10 @@ Authors ordered by first contribution:
|
||||
(none yet)
|
||||
|
||||
Image credits:
|
||||
Carlo Rodriguez <https://www.iconfinder.com/icons/512534/exercise_fitness_gym_gymnasium_icon>
|
||||
Vectorgraphit <https://www.iconfinder.com/vectorgraphit>
|
||||
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
@ -1,9 +1,965 @@
|
||||
v1.002 (28 Mar 2020)
|
||||
v2.0.0 (29 Feb 2020)
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
- Fix for PyPI installation problems
|
||||
MAJOR NEW FEATURES
|
||||
- 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
|
||||
|
||||
v1.0 (27 Mar 2020)
|
||||
MAJOR FIXES
|
||||
- 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 public release of GymBob
|
||||
This is the first release candidate for v2.0.0.
|
||||
|
||||
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 & 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
|
||||
|
||||
|
@ -3,4 +3,5 @@ recursive-include docs *
|
||||
recursive-include icons *
|
||||
recursive-include pack *
|
||||
recursive-include screenshots *
|
||||
recursive-include share *
|
||||
|
||||
|
983
README.rst
@ -1 +1 @@
|
||||
name = "gymbob"
|
||||
name = "tartube"
|
||||
|
@ -1 +1 @@
|
||||
#GymBob
|
||||
#Tartube
|
@ -1,758 +0,0 @@
|
||||
#!/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()
|
@ -1,75 +0,0 @@
|
||||
#!/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
|
||||
|
@ -1,901 +0,0 @@
|
||||
#!/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
|
@ -1,998 +0,0 @@
|
||||
#!/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()]
|
||||
|
@ -1,9 +1,39 @@
|
||||
COPYING
|
||||
|
||||
All files in ../win sub-directory
|
||||
All files in the ../dialogue sub-directory
|
||||
All files in the ../status sub-directory
|
||||
All files in the ../win sub-directory
|
||||
|
||||
Author: Carlo Rodriguez
|
||||
Source: https://www.iconfinder.com/icons/512534/exercise_fitness_gym_gymnasium_icon
|
||||
Author: Vectorgraphit
|
||||
Source: https://www.iconfinder.com/icons/199499/
|
||||
Author: bekeen studio
|
||||
Source: https://www.iconfinder.com/bekeenstudio
|
||||
|
||||
"Free for commercial use"
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
|
BIN
icons/dialogue/system_icon_64.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
icons/dialogue/system_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
icons/large/channel_both.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/channel_left.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
icons/large/channel_none.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
icons/large/channel_right.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/copy.png
Normal file
After Width: | Height: | Size: 958 B |
BIN
icons/large/folder_black_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/folder_black_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/folder_black_none.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/folder_black_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/folder_blue_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/folder_blue_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/folder_blue_none.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/folder_blue_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/folder_green_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/folder_green_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/folder_green_none.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/folder_green_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/folder_red_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/folder_red_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/folder_red_none.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
icons/large/folder_red_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/folder_yellow_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/folder_yellow_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/folder_yellow_none.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/large/folder_yellow_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/hand_left.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/hand_right.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/playlist_both.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/large/playlist_left.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
icons/large/playlist_none.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/large/playlist_right.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
icons/large/question.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/video_both.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
icons/large/video_left.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
icons/large/video_none.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
icons/large/video_right.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/large/warning.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
icons/locale/flag_uk.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
icons/small/archived.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/small/arrow_down.png
Normal file
After Width: | Height: | Size: 381 B |
BIN
icons/small/arrow_up.png
Normal file
After Width: | Height: | Size: 391 B |
BIN
icons/small/channel.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/small/check.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
icons/small/download.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
icons/small/error.png
Normal file
After Width: | Height: | Size: 543 B |
BIN
icons/small/folder.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
icons/small/folder_black.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/small/folder_blue.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/small/folder_green.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/small/folder_red.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/small/have_file.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
icons/small/no_file.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
icons/small/playlist.png
Normal file
After Width: | Height: | Size: 626 B |
BIN
icons/small/system_error.png
Normal file
After Width: | Height: | Size: 541 B |
BIN
icons/small/system_warning.png
Normal file
After Width: | Height: | Size: 765 B |
BIN
icons/small/video.png
Normal file
After Width: | Height: | Size: 621 B |
BIN
icons/small/warning.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
icons/status/status_check_icon_64.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
icons/status/status_check_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
icons/status/status_default_icon_64.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
icons/status/status_default_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 5.0 KiB |
BIN
icons/status/status_download_icon_64.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
icons/status/status_download_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
icons/status/status_info_icon_64.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
icons/status/status_info_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
icons/status/status_refresh_icon_64.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
icons/status/status_refresh_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
icons/status/status_tidy_icon_64.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
icons/status/status_tidy_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
icons/status/status_update_icon_64.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
icons/status/status_update_icon_xmas_64.png
Normal file
After Width: | Height: | Size: 6.0 KiB |
BIN
icons/toolbar/channel_large.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
icons/toolbar/channel_small.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
icons/toolbar/check_large.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
icons/toolbar/check_small.png
Normal file
After Width: | Height: | Size: 780 B |
BIN
icons/toolbar/download_large.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/toolbar/download_small.png
Normal file
After Width: | Height: | Size: 574 B |
BIN
icons/toolbar/folder_large.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/toolbar/folder_small.png
Normal file
After Width: | Height: | Size: 632 B |
BIN
icons/toolbar/playlist_large.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
icons/toolbar/playlist_small.png
Normal file
After Width: | Height: | Size: 626 B |
BIN
icons/toolbar/quit_large.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
icons/toolbar/quit_small.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
icons/toolbar/stop_large.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
icons/toolbar/stop_small.png
Normal file
After Width: | Height: | Size: 727 B |
BIN
icons/toolbar/switch_large.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
icons/toolbar/switch_small.png
Normal file
After Width: | Height: | Size: 811 B |