249 Commits
Author SHA1 Message Date
Jordan Petridis 88ea081661 Version bump. 2018-07-28 00:12:08 +03:00
Jordan Petridis 39c0a0dba5 Fix cargo vendor config. 2018-07-28 00:08:48 +03:00
Jordan Petridis 9d64d3e30d AppStream: captions were too short. 2018-07-27 21:45:33 +03:00
Jordan Petridis 39ff238716 Version bump. 2018-07-27 20:21:28 +03:00
Jordan Petridis 7c96152f3f Gtk: Change the minimum requests of the views. 2018-07-27 19:46:18 +03:00
Jordan Petridis a2440c19e1 Appdata: Fix screenshots metadata. 2018-07-27 00:42:54 +03:00
Jordan Petridis 24dff5ce85 Update the release script. 2018-07-27 00:27:42 +03:00
Tobias Bernard 1ca8e15d19 Update org.gnome.Podcasts.appdata.xml 2018-07-26 21:15:10 +00:00
Tobias Bernard f77a8f09bb Update podcasts.doap 2018-07-26 21:13:04 +00:00
Jordan Petridis 48a7c8140f Update screenshots. 2018-07-26 23:53:08 +03:00
Jordan Petridis b03ff46767 Version bump. 2018-07-26 08:33:45 +03:00
Jordan Petridis e66e6364c3 Update Changelog 2018-07-26 05:30:46 +00:00
Jordan Petridis 89ef7ac4f6 Rename the .doap file. 2018-07-26 07:56:00 +03:00
Jordan Petridis 27e74ee064 Update the appdata summary. 2018-07-26 07:53:29 +03:00
Jordan Petridis 83c44aa12c Replace links again 2018-07-26 07:41:43 +03:00
Jordan Petridis 17da62d53b App: Fix some refference cycles. 2018-07-26 07:19:43 +03:00
Jordan Petridis d43fc268f4 App: Only execute .show_all() at startup.
If you would had an instance running and activate the app again
the UI would break visually in all kind of ways.
2018-07-26 07:07:56 +03:00
Jordan Petridis 5a7ab9795d Headerbar: rename menu-button to hamburger. 2018-07-25 04:57:00 +03:00
Jordan Petridis cfcdba5aea Headerbar: Avoid code duplication and ref cycles. 2018-07-25 04:38:24 +03:00
Jordan Petridis aaca6a6704 Prefs: Use weak refs to avoid refference cycle. 2018-07-25 04:25:40 +03:00
Jordan Petridis 5f7c822deb gtk: Rename left over home_view .ui files. 2018-07-25 03:55:42 +03:00
Jordan Petridis 53be091a31 Replace links 2018-07-25 03:26:41 +03:00
Jordan Petridis 04c68ba013 Initial massive renaming. 2018-07-25 03:26:35 +03:00
Jordan Petridis 518ea9c8b5 EmptyView: Improve wording of strings and Center stuff. 2018-07-24 15:07:13 +03:00
Jordan Petridis 6bb2142f35 Prefs: Fix typo. 2018-07-24 15:06:56 +03:00
Jordan Petridis 6aa931c866 Update deps. 2018-07-24 09:28:25 +03:00
Jordan Petridis 67ab54f820 Headerbar: Try to improve the add button behavior. 2018-07-24 09:05:55 +03:00
Jordan Petridis cc9fc80328 Remove some unwraps. 2018-07-24 08:04:31 +03:00
Jordan Petridis 3e8a8a6b85 Hamburger: connect the MenuModel during initialization. 2018-07-24 07:36:41 +03:00
Jordan Petridis 70a2d0e5f3 EpisodeWidget: Improve total_size label wording. 2018-07-24 07:29:52 +03:00
Jordan Petridis 09e8d7e1da AboutDialog: Update authors list. 2018-07-24 07:25:57 +03:00
Jordan Petridis 3c3d6c1e7f EmptyView: Style improvments.
Suggest using the OPML import action from the hamburger menu.

Minor rework of the over style.
2018-07-24 07:23:00 +03:00
Jordan Petridis 0dcc95cd34 Hamburger: Hide unimplemented menu actions. 2018-07-24 06:47:43 +03:00
Jordan Petridis edae1b0480 Add Application Icons.
Huge thanks to Tobias and Sam for these!
2018-07-24 06:27:34 +03:00
Jordan Petridis 5fb2cb7e76 Merge branch 'prefs' into 'master'
Prefrences dialog

See merge request World/hammond!34
2018-07-24 02:57:29 +00:00
Jordan Petridis 403bb71c5d Prefs: Bikeshed on the naming of things. 2018-07-24 05:05:52 +03:00
Jordan Petridis 9e23b16ae7 App: Remove no longer needed allow(unused). 2018-07-24 05:05:43 +03:00
Jordan Petridis 60a93a2433 App: Use the gio::prelude 2018-07-24 05:05:39 +03:00
Zander Brown 14dfafcb7c Sort out accelerators 2018-07-24 05:05:34 +03:00
Zander Brown dcc06cf8c6 Initial dialog 2018-07-24 05:05:30 +03:00
Zander Brown 54c084040c Minor rustfmt update 2018-07-24 05:05:26 +03:00
Zander Brown a9c38f5a03 Remove the update section 2018-07-24 05:05:22 +03:00
Zander Brown bce80cca0b Fix some style issues 2018-07-24 05:05:19 +03:00
Zander Brown f56fac6877 Bind GtkSettings dark property to GSettings
Now you can switch between dark and light on the fly
2018-07-24 05:05:15 +03:00
Zander Brown ef2286dca4 Bind refresh/cleanup rate 2018-07-24 05:05:11 +03:00
Zander Brown a4a012368e Bind switches to keys 2018-07-24 05:05:08 +03:00
Zander Brown 1b623ef346 Initial dialog 2018-07-24 05:05:00 +03:00
Jordan Petridis f661d24544 Merge branch 'icons-are-weird' into 'master'
Get the symbolics to work

See merge request World/hammond!45
2018-07-24 01:53:01 +00:00
Zander Brown cfe10553b2 Stop overriding the images 2018-07-24 04:01:16 +03:00
Jordan Petridis cf1042f40d Player: Improve the tooltips of buttons. 2018-07-23 23:36:52 +03:00
Jordan Petridis e77000076b Player: Add custom icons for the forward and rewind buttons. 2018-07-23 21:28:29 +03:00
Jordan Petridis 49241664dc Merge branch 'alatiera-master-patch-41773' into 'master'
Add Sam to the artists credits.

See merge request World/hammond!44
2018-07-21 23:19:11 +00:00
Jordan Petridis ffa3e9ec9a Add Sam to the artists credits. 2018-07-21 23:02:56 +00:00
Jordan Petridis fbbe0d9ca9 EpisodeWidget: Tweak padding and spacing. 2018-07-21 21:14:07 +03:00
Jordan Petridis 001eeecc09 ShowMenu: Add a separator and set alignment left. 2018-07-21 20:12:02 +03:00
Jordan Petridis 454a9c7076 ShowWidget: Fix description padding. 2018-07-21 19:41:15 +03:00
Jordan Petridis fc934ce8e1 ShowsView/Stack: Add some assertions. 2018-07-21 10:39:42 +03:00
Jordan Petridis b9bcc28e0f ShowMenu: Add debug assertions here too. 2018-07-21 10:30:46 +03:00
Jordan Petridis b5ddca65f5 ShowWidget: Add some assertions. 2018-07-21 10:24:48 +03:00
Jordan Petridis 536805791e ShowWidget: Move controls into a headerbar menu.
This fits better the HIG and allows for more flexibility in the way
the ShowWidget is implemented/designed.
2018-07-21 09:47:08 +03:00
Jordan Petridis 5a6c73c4c1 Headerbar: Move/Rename the menus.ui to hamburger.ui 2018-07-21 07:20:48 +03:00
Jordan Petridis d50f5a0488 Player: Remove no longer relavant FIXME annotations. 2018-07-19 22:14:17 +03:00
Jordan Petridis 671a31a95a Player: Improver human-facing error message. 2018-07-18 16:16:33 +03:00
Jordan Petridis 5e38f41530 Merge branch 'lazy_drawing' into 'master'
Lazy drawing

See merge request World/hammond!43
2018-07-18 13:14:44 +00:00
Jordan Petridis 7569465a61 App: Remove the imposed delay before refresh_on_startup runs.
The application is even lazier now and this is no longer an issue.
2018-07-17 20:42:57 +03:00
Jordan Petridis 5913166a13 EpisodeWidget: Avoid Refference Cycles.
When passing an Rc into a gtk callback it causes
the Rust struct to be kept in memory even if the gtk+ wiget
was dropped.

Should have been using Weak refferences all along.
2018-07-17 20:42:45 +03:00
Jordan Petridis 6036562af2 HomeEpisode: Do not initialize the Image until it needs to be drawn. 2018-07-17 16:08:20 +03:00
Jordan Petridis 39e6c258d5 ShowsChild: do not initialize the cover until it needs to be drawn. 2018-07-17 16:04:04 +03:00
Jordan Petridis 1ab0291483 EpisodeWidget: Only initialize the episode once it's first drawn. 2018-07-17 15:57:52 +03:00
Jordan Petridis fe024502d4 EpisodeWidget: Pass the EpisodeModel by value.
This is preparationg for being able to pass the model to
the connect_craw callback directly.
2018-07-16 16:01:32 +03:00
Jordan Petridis 9a76c6428a Merge branch 'hammond-service-launch'
See merge request !42
2018-07-15 22:21:18 +03:00
Jordan Petridis 2d4053c792 cargo fmt 2018-07-15 22:20:45 +03:00
Zander Brown a69254612c Install as a DBus service 2018-07-15 22:20:31 +03:00
Zander Brown 09a14c1270 Delay showing window until ::activate
Bit of a hack as we are still creating the window in ::startup but it's good compromise

Pass the arguments to GApplication so we can be launched as a service
2018-07-15 22:20:28 +03:00
Jordan Petridis b343068805 cargo fmt 2018-07-15 22:17:00 +03:00
Jordan Petridis ecf50dde2b Update .gitignore 2018-07-15 22:16:06 +03:00
Jordan Petridis 008404ffb3 Update Changelog. 2018-07-04 18:40:03 +03:00
Jordan Petridis 2b6cca6bab Version bump. 2018-07-04 17:26:56 +03:00
Jordan Petridis fe968e19c0 Update Changelog. 2018-07-04 16:39:19 +03:00
Jordan Petridis 479498d8be EpisodeMinimal: add lenght as well. 2018-07-03 16:38:08 +03:00
Jordan Petridis af9669acd0 Add a bash script to run the test-suite. 2018-07-01 00:55:57 +03:00
Jordan Petridis d2eb98f859 Update gitignore file. 2018-07-01 00:54:28 +03:00
Jordan Petridis ae11084f48 Merge branch 'db-cleanup' into 'master'
Database cleanup

See merge request World/hammond!41
2018-06-30 21:25:51 +00:00
Jordan Petridis b02b554105 Models: Change the Query suffix to Model.
This is maps better to the MVC naming convention.
2018-06-30 23:02:13 +03:00
Jordan Petridis 2d66ba918a Models: Rename Podcat Models to Show to better match the UI code. 2018-06-30 22:47:58 +03:00
Jordan Petridis 400c0f35f0 PodcastModel: Remove dead code. 2018-06-30 22:20:07 +03:00
Jordan Petridis 5b8b265371 Database: Add diesel_cli config. 2018-06-30 21:47:57 +03:00
Jordan Petridis f3fb27005a Database: Rename the tables to better match the userfacing strings 2018-06-30 21:47:49 +03:00
Jordan Petridis 79bb9bdde8 Database: Drop dead fields/columns.
If and when such featured are going to be implemented, it should be
trivial to re-add them. Till then there is no reason for them to exist.
2018-06-30 18:05:52 +03:00
Jordan Petridis 4b983e401d PlayerWidget: Use weak ref counting for callbacks.
When we pass strong reference to callback closures, it prevents
the object to be dropped even if they gtk Widget was destroyed.
2018-06-27 23:06:27 +03:00
Jordan Petridis 5f2f0a9a57 Headerbar: Hide the hamburger button when not in a global view. 2018-06-27 20:27:16 +03:00
Jordan Petridis 8b2ae6d464 Headerbar: Remove the requirment of a window to construct it. 2018-06-27 20:19:03 +03:00
Jordan Petridis 91aae6a9f5 Headerbar: Factor out the AddPopover widget. 2018-06-27 20:15:42 +03:00
Jordan Petridis b0fc9ef05e Headerbar: Factor out the Update Indicator widget. 2018-06-27 18:51:56 +03:00
Jordan Petridis 5d6fbb6f04 Headerbar: ::new() method now returns Rc<Self>. 2018-06-27 18:20:45 +03:00
Jordan Petridis 301ebdbcd8 Content: ::new() method now returns Rc<Self>. 2018-06-27 18:18:11 +03:00
Jordan Petridis 49bcf46b4f app.rs: Group the gactions declarations. 2018-06-27 17:11:32 +03:00
Jordan Petridis f7263c8ab8 app.rs: Move the action channel to it's own function. 2018-06-27 16:48:35 +03:00
Jordan Petridis c69772131a app.rs: Refactor into an object/struct again. 2018-06-27 16:32:08 +03:00
Jordan Petridis e8c025b898 app.rs: Minor style change. 2018-06-26 23:37:14 +03:00
Jordan Petridis 8fb5c16bce Upgrade crossbeam-channel. 2018-06-26 23:37:06 +03:00
Jordan Petridis f4551ddf3a Update deps. 2018-06-26 23:37:02 +03:00
Jordan Petridis f337488951 Readme: Update the dependencies needed 2018-06-25 17:35:54 +00:00
Jordan Petridis f104f11613 Fix trivial_cast lint warning. 2018-06-24 20:01:27 +03:00
Jordan Petridis 32e8f952fd Even more lints! 2018-06-24 02:21:27 +03:00
Jordan Petridis c7cfc81c6f Merge branch 'embedded-player' into 'master'
Embedded player

Closes #38

See merge request World/hammond!40
2018-06-23 23:03:39 +00:00
Jordan Petridis faeafc329c PlayerWidget: Tweak rewind on pause behavior.
Only rewind on pause if the stream position is passed a certain point.
Else it can feel a bit weird if you just started the stream and it
immediatly rewinds.
2018-06-23 22:45:21 +00:00
Jordan Petridis eeef0d13ff PlayerWidget: Use the Gtk Main Context for the gst_player as well.
There is no longer a need for sending stuff to the main-thread `Action`
channel anymore thanks to this.
2018-06-23 22:45:21 +00:00
Jordan Petridis 0686fca3b0 h-gtk: Increase the polling rate of the main thread channel.
When dragging the PlayerWidget.timer.slider widget PlayerDurationChanged
messaged pile up in the channel and get out of sync with the
slider.connect_value_changed() signal.
2018-06-23 22:45:21 +00:00
Jordan Petridis 79b425326b Update Changelog. 2018-06-23 22:45:20 +00:00
Jordan Petridis 2d879b9604 PlayerRate: Change the container widget to GtkBox and add padding. 2018-06-23 22:45:20 +00:00
Jordan Petridis 38eb14b013 Delete commented out code. 2018-06-23 22:45:19 +00:00
Jordan Petridis ff2f43766e PlayerWidget: Add a widget to change the playback speed of the stream.
Only 3 options are offered currently since the design of the feature
is still in progress and this is only a throw a away prototype.
2018-06-23 22:45:19 +00:00
Jordan Petridis 593d66ea54 EpisodeWidget: Mark an episode as played when the play button is hit.
Ideally episodes would be marked as played only when they have
passed a cerain point in their duration, but till thats ready
we should keep marking them.
2018-06-23 22:45:19 +00:00
Jordan Petridis ee8cbbf7ef PlayerWidget: Delete commented out stuff. 2018-06-23 22:45:19 +00:00
Jordan Petridis 474cb49d2c PlayerInfo: Increase the size of the cover. 2018-06-23 22:45:19 +00:00
Jordan Petridis 590f815dc0 PlayerInfo: Limit label widths and add tooltips. 2018-06-23 22:45:18 +00:00
Jordan Petridis a93d5246d2 PlayerInfo: Swap bold properties of the labels. 2018-06-23 22:45:18 +00:00
Jordan Petridis 3e2ab8e7ee PlayerExt: Add a stop method.
Also connect on the gst_player::Player::connect_on_stream_end method.

The main reason for this is to be able to reset the slider bar to 0
upon a stream ends.
2018-06-23 22:45:18 +00:00
Jordan Petridis a83270699f PlayerWidget: refactor seek method. 2018-06-23 22:45:17 +00:00
Jordan Petridis 745afb32a3 PlayerWidget: Rewind on pause. 2018-06-23 22:45:17 +00:00
Jordan Petridis 2fcb8d915d PlayerWidget: Remove an .expect() occurrence. 2018-06-23 22:45:17 +00:00
Jordan Petridis a596b62a5f PlayerWidget: Tweak gst_player config. 2018-06-23 22:45:16 +00:00
Jordan Petridis 55b1504aab PlayerTimes: Display human-friendly values. 2018-06-23 22:45:16 +00:00
Jordan Petridis c42822669b PlayerTimes: Replace unnecessary Arc with Rc. 2018-06-23 22:45:16 +00:00
Jordan Petridis b58d28c723 PlayerTimes: create wrapper struct of gst::ClockTime.
Now on_duration_changed requires a `Duration` type and
on_position_updated requires a `Position` type as oppose to
both accepting `ClockTime` as their argument.
2018-06-23 22:45:16 +00:00
Jordan Petridis a6a34d8246 PlayerWidget: Group the button connect_clicked methods. 2018-06-23 22:45:16 +00:00
Jordan Petridis 0080399db2 PlayerWidget: Move on_duration_change and on_postion_updated methods.
Previously they depended on the player/pipeline to get the ClockTime
values, and only `PlayerWidget` had access to the `gst_player::Player`
object.

Now that it uses the gst_player methods instead of the raw pipeline
methods to get the ClockTime values it no longer needs access to the whole
PlayerWidget object.
2018-06-23 22:45:15 +00:00
Jordan Petridis 50b480ee23 PlayerWidget: Refactor the position_changed/updated callback.
It now uses gst_player::Player::connect_position_updated callback
to send, cross threads, the `position` value to the gtk main loop
which then updates the widget.
2018-06-23 22:45:15 +00:00
Jordan Petridis da467b7837 PlayerWidget::seek handle the case where the offset might be none. 2018-06-23 22:45:15 +00:00
Jordan Petridis 70914b6c3e PlayerWigdet: Refactor the way the duration label is updated.
This now connect's directly to gst_player::Player::connect_duration_changed
method.

The method then sends a cross-thread msg to the Action channel in the main loop that
then updates the widget.
2018-06-23 22:45:14 +00:00
Jordan Petridis 6c3fbfe0ca PlayerWiget: refactor the seekbar connect signal. 2018-06-23 22:45:14 +00:00
Jordan Petridis 8e4b705e60 PlayerWidget: Log the gst warnings. 2018-06-23 22:45:14 +00:00
Jordan Petridis 48d80d3194 PlayerWidget: Remove unused vars an Enum. 2018-06-23 22:45:14 +00:00
Jordan Petridis 38768c777d PlayerWidget: Connect the fast-forward and rewind buttons, sort of.
There appears to be a bug where it seeks 17 seconds instead of 10.
2018-06-23 22:45:13 +00:00
Jordan Petridis 1daa841f31 PlayerWidget: Connect to the errors callback. 2018-06-23 22:45:13 +00:00
Jordan Petridis a9f81d0ad3 PlayerWidget: Refactor the timers callbacks.
Should use the gst_player::Player callbacks instead but they require
the Send Trait which means we would need to use SendCell and that's
not something I am going to deal with right now.
2018-06-23 22:45:13 +00:00
Jordan Petridis 76720424ab PlayerWidget: Set a custom config for the gst Player. 2018-06-23 22:45:13 +00:00
Jordan Petridis a7b639a66b PlayerWidget: Wire the PlayerTimes labels and scale.
Adapted from gstreamer basic-tutorial-5.
https://gstreamer.freedesktop.org/documentation/tutorials/basic/toolkit-integration.html
2018-06-23 22:45:12 +00:00
Jordan Petridis 1b78d221b6 PlayerWidget: Wire the play and pause buttons and add style classes to the Info Labels.
This also includes the yak shaving of a ::new and ::inti methods.
2018-06-23 22:45:12 +00:00
Jordan Petridis ac7b1a3c66 CI: disable debian builds fow now.
Debian stable provides gst 1.10 but the gst-rs bindings requiere
v 1.12 to build.

I will make custom images Soon™
2018-06-23 22:45:12 +00:00
Jordan Petridis 039c3182aa h-gtk: Remove unused .ui file. 2018-06-23 22:45:12 +00:00
Jordan Petridis 55d94b1844 CI: Add gstreamer as a dep for the debian build. 2018-06-23 22:45:11 +00:00
Jordan Petridis 3baa69b43b cargo fmt 2018-06-23 22:45:11 +00:00
Jordan Petridis 5f92df97e6 PlayerWidget: Wire the widget to the GUI.
This commit also removes the majority of the playback widget,
though most of it's code will make it to the PlayerWidget once
it starts to get wired to the gtreamer_plaer::Player.
2018-06-23 22:45:11 +00:00
Jordan Petridis 47f297c495 PlayerWidget: Intial draft of the the PlayerExt trait. 2018-06-23 22:45:11 +00:00
Jordan Petridis 58f09ba150 h-gtk: Bind the new player widget to code. 2018-06-23 22:45:10 +00:00
Jordan Petridis 1142948945 Rework the player widget. 2018-06-23 22:45:10 +00:00
Zander Brown 9528160b03 Start hooking things up
Still doesn't accept input
2018-06-23 22:45:10 +00:00
Zander Brown 4afdc54914 Initial playback control area
(not plumbed in)
2018-06-23 22:45:10 +00:00
Zander Brown 09973a6a56 Initial playback
... and not a lot more. Hit play and the podcast will play, press play on something else and that will play instead
2018-06-23 22:45:09 +00:00
Jordan Petridis f56bf3afef Enable more rustc lints 2018-06-23 21:02:19 +03:00
Jordan Petridis c4c7bbf46b Content: Change the user-facing string of the home stack. 2018-06-23 16:11:00 +03:00
Jordan Petridis 7fdd374911 Update the gitlab bug issue template 2018-06-23 13:01:34 +00:00
Jordan Petridis fb9ad9870d Update the gitlab feature issue template. 2018-06-23 12:59:35 +00:00
Jordan Petridis bbabc6f5e9 h-gtk: Add resources/ to the cargo check list. 2018-06-11 14:23:29 +03:00
Jordan Petridis 2060579bb4 gitlab templates: Fix markdown formatting. 2018-06-09 16:16:57 +03:00
Jordan Petridis b0f0940605 Merge branch 'master' into 'master'
Our GActions don't need to be in the app namespace

See merge request World/hammond!39
2018-06-07 17:37:12 +00:00
Zander Brown ee23df176d Our GActions don't need to be in the app namespace 2018-06-07 18:06:36 +01:00
Jordan Petridis d53865d81b Update deps. 2018-06-07 16:14:40 +03:00
Jordan Petridis 7becfd8adb Commit Updated Cargo.lock.
This was part of a9feed56fe but
forgot to commit it.
2018-06-07 15:47:41 +03:00
Jordan Petridis a9feed56fe Replace html2pango with html2text. 2018-06-06 15:28:44 +03:00
Jordan Petridis dea517c17c Update the Changelog. 2018-06-05 14:58:29 +03:00
Jordan Petridis 6d93ceb910 EpisodeWidget: Minor style change. 2018-06-05 14:33:18 +03:00
Jordan Petridis 9b0ac5b83d EpisodeWidget: Do not lock the Proggress struck when running update callbacks.
Previously each time we wanted to inspect the `Progress` struct we
were blocking which was problematic since the downloader also wants
to block to update it.

Now we use try_lock() and if a lock can't be aquired we requeue another
callback. That way we can also be way more aggressive about the interval
in whihc it the callbacks will run.
2018-06-05 14:17:37 +03:00
Jordan Petridis acabb40171 CI and Flatpak: Use the 3.28 runtime till fdo 1.8 is fixed. 2018-06-04 14:25:38 +00:00
Jordan Petridis 04cd56ca16 CI: change the image of the flatpak job. 2018-06-02 20:55:09 +00:00
Jordan Petridis 4371512ba2 Merge branch 'episode-wiget-refactor' into 'master'
Episode wiget refactor

See merge request World/hammond!38
2018-06-02 20:42:46 +00:00
Jordan Petridis ced686e1cd EpisodeWidget: Remove explicit type declarations. 2018-06-02 22:33:00 +03:00
Jordan Petridis 272aab2397 EpisodeWidget: Document determine_state method. 2018-06-02 22:00:30 +03:00
Jordan Petridis a7f87f2ac8 ShowWidget: Fix markallplayed functionality.
cfe79a73d6 changed the structure
of the EpisodeWidget and broke this.
2018-06-02 21:34:19 +03:00
Jordan Petridis f9e85155a8 Remove unused dependancy. 2018-06-02 21:13:30 +03:00
Jordan Petridis d281c18951 EpisodeWidget: Pass EpisodeWidgetQuery by refference. 2018-06-02 21:03:07 +03:00
Jordan Petridis 04e7f4f8a7 EpisodeWidget: Wire the download_checker callback again.
If an episode is being downloaded we setup a callback that will
supervise the widget and update it's state once the download action is
completed and the episode rowid is removed from `manager::ACTIVEDOWNLOADS`.
2018-06-02 21:03:00 +03:00
Jordan Petridis a090c11f4a EpisodeWidget: Wire the progressbar again. 2018-06-02 19:42:19 +03:00
Jordan Petridis c303c697a9 EpisodeWidget: Wire the total_size label again.
The size might be provided by the rss feed but not alwasy. Additionally it might be
missleading so when a download starts we replace the label with the
HTTP ContentLength header.
2018-06-02 19:25:25 +03:00
Jordan Petridis 9466c5ea10 EpisodeWidget: Wire the cancel button. 2018-06-01 21:30:56 +03:00
Jordan Petridis 1268fcf1cc EpisodeWidget: Wire the download button. 2018-06-01 19:08:56 +03:00
Jordan Petridis 86d06fa879 EpisodeWidget: Wire the play button again. 2018-06-01 16:49:06 +03:00
Jordan Petridis cfe79a73d6 EpisodeWidget: Initial refactor.
State machines were a fun experiement but a nightmare to deal with
after the fact. This is the first steps for a refactor in a tradition
style with the goal to ultimatly making it easy to and port to relm.
2018-06-01 16:19:33 +03:00
Jordan Petridis d7a9d9ddc8 CI: FIx the clippy cache. 2018-05-31 14:46:51 +03:00
Jordan Petridis b3d45384e1 meson: Add debug build. 2018-05-31 14:45:54 +03:00
Jordan Petridis 64099e37e5 Merge branch 'dialog-enchance' into 'master'
Avoid un-closable AboutDialog

See merge request World/hammond!37
2018-05-31 09:42:52 +00:00
Zander Brown 2fe612d392 Avoid un-closable AboutDialog 2018-05-31 08:35:09 +00:00
Jordan Petridis 14d72b92cb h-gtk: Move appnotif.rs into the widgets module. 2018-05-30 16:45:46 +03:00
Jordan Petridis 8c0055723c cargo fmt 2018-05-30 16:25:15 +03:00
Jordan Petridis bb9e368b2d Merge branch 'ZanderBrown/hammond-gaction_macro' 2018-05-29 22:21:55 +03:00
Zander Brown 2c203acbd2 Use a macro when setting up simple SimpleActions 2018-05-29 17:16:05 +00:00
Jordan Petridis 7115eb573c Downgrade Diesel to 1.2.x
disel_migrations 1.3 triggeres some clippy lints atm.
2018-05-29 18:16:57 +03:00
Jordan Petridis e626c6f286 app.rs: Factor out the GAction definitions. 2018-05-29 14:04:09 +03:00
Jordan Petridis 24058f9534 h-gtk: Write doc comment for aboutdialog. 2018-05-29 13:36:22 +03:00
Jordan Petridis a8d47e9a72 app.rs: Remove unused variable. 2018-05-29 13:26:36 +03:00
Jordan Petridis 9a2f51b48d Update deps. 2018-05-28 22:06:07 +03:00
Jordan Petridis 667deef5f2 Use a mpmc channel instead of the mspc from the std. 2018-05-28 20:49:12 +03:00
Jordan Petridis aa349aa935 Flatpak: Remove access to XDG_HOME.
In 75c50392cb we switched to use the
FileChooserNative API which means that filesystem access is automatically
handled by the Documents(!)(Might be another one) xdg portal.
2018-05-28 19:50:25 +03:00
Jordan Petridis 666ab01d03 Merge branch 'appmenu' into 'master'
Appmenu

See merge request World/hammond!33
2018-05-28 16:41:25 +00:00
Zander Brown ffbab0136f Bind F10 to open the menu
Because we aren't using app-menu accels aren't automatically binded
2018-05-27 15:48:27 +01:00
Zander Brown b5f7399b2c RIP appmenu
F5 -> <primary>r for refresh
2018-05-27 14:34:58 +01:00
Zander Brown f1892eeba2 Always show hamburger menu 2018-05-22 10:46:50 +01:00
Zander Brown e7128a57db Resolve some comments 2018-05-22 10:28:13 +01:00
Zander Brown 793cafd294 Formatting updates 2018-05-22 09:55:00 +01:00
Zander Brown 9b1097effe Merge branch 'master' of https://gitlab.gnome.org/World/hammond into appmenu 2018-05-22 09:51:31 +01:00
Zander Brown 079ae0e1f3 Fallback to hamburger when the environment doesn't like app menus 2018-05-21 13:01:06 +01:00
Zander Brown e181a9837a Merge upstream master 2018-05-21 12:01:32 +01:00
Zander Brown ca5c7022ef Fixed some shortcut display issues
Also give FileChooserNative arguments in the right order & add F5 to refresh
2018-05-21 11:49:35 +01:00
Zander Brown 75c50392cb Everything works (ish)
Also use FileChooserNative for flatpak nicities
2018-05-21 10:06:10 +01:00
Jordan Petridis 74f8e744ac Version bump. 2018-05-20 17:47:33 +03:00
Jordan Petridis 1c657036da Update Changelog. 2018-05-20 17:40:27 +03:00
Jordan Petridis 2869bb3ef3 Flatpak: Allow the app to access the Home folder.
This is needed in order to import an OPML file. There
might be a better solution to this.
2018-05-20 17:37:48 +03:00
Jordan Petridis 784e117a8a cargo fmt 2018-05-20 16:39:36 +03:00
Zander Brown 8c2ea052de Keyboard shortcut overview!
(shame everything else is broken...)
2018-05-20 13:59:00 +01:00
Jordan Petridis 0b6bbf6733 CI: Fix rustfmt job. 2018-05-20 14:36:20 +03:00
Jordan Petridis 2312df3718 Fix rustfmt config. 2018-05-20 14:32:06 +03:00
Zander Brown 095dd73c52 Move refresh 2018-05-19 22:11:44 +01:00
Zander Brown ac6ac42860 Move import 2018-05-19 21:48:38 +01:00
Zander Brown c6ce888cc7 Define an app-menu with About & Quit actions
Rename some paths for auto resource magic
2018-05-19 20:38:36 +01:00
Jordan Petridis c4e6fcc451 Version bump. 2018-05-19 13:11:02 +03:00
Jordan Petridis 22ae5242b2 Update Changelog. 2018-05-19 12:44:14 +03:00
Jordan Petridis 4d77281249 Update deps. 2018-05-19 12:17:55 +03:00
Jordan Petridis b77a373efa Opml: remvoe uneccesary to_string invocation. 2018-05-17 15:08:38 +03:00
Jordan Petridis 463c3908d4 Revert "gitlabci: switch to test rust-nightly till the stable image is update."
This reverts commit 2711ebca68.

Rust stable image is updated again.
2018-05-16 21:00:35 +03:00
Jordan Petridis 79239a2e01 gitlabci: Cache cargo for the clippy job. 2018-05-16 20:59:54 +03:00
Jordan Petridis 41073615e9 Clippy: Derive Copy for appnotif::UndoState 2018-05-16 19:54:07 +03:00
Jordan Petridis bd12b09cbc ShowsView: Fix a bug where the last show would not be shown.
utils::lazy_load() now calls widget.show() for each widget it adds
to the parent container.
2018-05-16 19:44:58 +03:00
Jordan Petridis 7d598bb1d0 h-gtk: Smooth out some stack transitions. 2018-05-16 19:40:36 +03:00
Jordan Petridis ccd3e3ab2c h-gtk: Show error notifications when OPML import fails. 2018-05-16 17:54:32 +03:00
Jordan Petridis 118dac5a1a app.rs: Add an action for showing error notification. 2018-05-16 17:30:43 +03:00
Jordan Petridis 7035fe05c4 InAppNotification: Extend the widget to allow showing notifications without an undo button. 2018-05-16 16:58:03 +03:00
Jordan Petridis af7331c6c6 h-gtk: Rename EpisodeViewWidget to HomeEpisode. 2018-05-16 16:33:54 +03:00
Jordan Petridis 4d2b64e79d h-gtk: Remember the vertical allingment of the ShowsView. 2018-05-13 22:47:56 +03:00
Jordan Petridis d47bbd6131 Remove explicit and not needed inline calls.
This code is not performance critical and the compiler will already
inline whatever it thinks it might benefit it.
2018-05-13 22:08:25 +03:00
Jordan Petridis 54fafa07a2 h-gtk: Use clone! macro to replace some boilerplate. 2018-05-13 21:51:32 +03:00
Jordan Petridis 97eef9149c import_dialog: Do not show hidden files in the FileChooser. 2018-05-13 00:22:54 +03:00
Jordan Petridis b95e70a8c4 import_dialog: Only show xml files in the FileChooser. 2018-05-13 00:06:35 +03:00
Jordan Petridis a16d7de1ac Add EPIC.md issue template. 2018-05-12 23:15:53 +03:00
Jordan Petridis 6f590a64af gitlabci: Fix review-app. 2018-05-12 23:01:50 +03:00
Jordan Petridis 00e747eb5f h-gtk: Wire the import_shows button on the hamburger menu to the the opml import. 2018-05-12 22:55:35 +03:00
Jordan Petridis 2d8164cf0a Clippy: remove useless attribute.
Also merge some attributes together.
2018-05-12 15:02:33 +03:00
Jordan Petridis be1a8df3ef Headerbar: simplify the naming scheme a bit.
The type of the widgets is already a Button so there's no need in
repeating that in the struct field.

Also remove some type annotations since the compiler can infer them
from the type of the struct fields.
2018-05-12 14:45:00 +03:00
Jordan Petridis e8ca2faaa7 Headerbar: Add import and export items in the hamburger menu.
I strongly believe that these do not belong there and should be moved
elsewhere. But for the time being and prototyping they are "good enough".

People most of the time tend to import from an OPML file only on the first
use of the App. Then the functionality sits there and is never used again.
That's why I think import should be moved into a preferences dialogs and
have the empty view suggest the action.

Exporting OPML files is also not that common, I also believe this should be
moved into a preference dialog instead of the hamburger menu.
2018-05-12 14:17:40 +03:00
Jordan Petridis 83f9284b05 Cargo fmt 2018-05-12 13:45:19 +03:00
Jordan Petridis 6e5c70ab71 opml: Change the signature of import_opml function.
xml::reader::Error is the only error that can be returned so there
is no need to use the DataError type.
2018-05-12 13:19:14 +03:00
Jordan Petridis ab4f958b5f Readme: bump the minimum rust version. 2018-05-11 13:55:34 +03:00
Jordan Petridis 2711ebca68 gitlabci: switch to test rust-nightly till the stable image is update. 2018-05-11 13:54:25 +03:00
Jordan Petridis 041684b13a Use impl Trait syntax instead of Trait Objects.
Rust v1.26 introduced impl Trait which avoid the heap allocation with Box
and makes the code a bit more ergonomic.

For more see https://blog.rust-lang.org/2018/05/10/Rust-1.26.html
2018-05-11 12:06:31 +03:00
Jordan Petridis 777a2102f8 gitlabci: Add review apps.
This makes it able to show a link pointing to the bundle in Merge Requests.
2018-05-10 18:46:52 +03:00
Jordan Petridis 65949c5af5 gitlabci: Extend the lifespan of the flatpak bundle.
For more information see the following nautilus commit.
https://gitlab.gnome.org/GNOME/nautilus/commit/3a35b6035ae6f383744aa88b73e451918292736c

Closes #60.
2018-05-10 18:46:52 +03:00
Jordan Petridis e40f880b9e gitlabci: Use prebuilt clippy image
Since clippy won't fail to install now that it's bundled in the
container image also do not allow the to fail
2018-05-10 18:38:51 +03:00
Jordan Petridis f9b34bbd50 h-data: Initial implementation of an OPML parser and importer.
This is not really compiant with the OPML spec and there
does not seem to be an OPML crate sadly. There are edge-cases
that are not handled but will only be addressed if a problem is reported.
2018-05-10 18:17:19 +03:00
135 changed files with 7153 additions and 4328 deletions
+4 -3
View File
@@ -1,9 +1,10 @@
target/
**/*.rs.bk
Cargo.lock
.vscode
*.ui~
resources.gresource
_build
_build/
build/
vendor/
.criterion
.criterion/
org.gnome.*.json~
+66 -34
View File
@@ -1,6 +1,20 @@
stages:
- test
- lint
- review
variables:
BUNDLE: "gnome-podcasts-dev.flatpak"
.cargo_cache_template: &cargo_cache
cache:
# JOB_NAME - Each job will have it's own cache
# COMMIT_REF_SLUG = Lowercase name of the branch
# ^ Keep diffrerent caches for each branch
key: "$CI_JOB_NAME"
paths:
- target/
- .cargo_cache/
.cargo_test_template: &cargo_test
stage: test
@@ -11,7 +25,7 @@ stages:
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson libgstreamer1.0-dev
- mkdir -p .cargo_cache
# Only stuff inside the repo directory can be cached
@@ -20,27 +34,19 @@ stages:
script:
- rustc -Vv && cargo -Vv
# Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cd podcasts-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo build
- cargo test -- --test-threads=1
- cargo test -- --test-threads=1 --ignored
<<: *cargo_cache
cache:
# JOB_NAME - Each job will have it's own cache
# COMMIT_REF_SLUG = Lowercase name of the branch
# ^ Keep diffrerent caches for each branch
key: "$CI_JOB_NAME"
paths:
- target/
- .cargo_cache/
rust:stable:
.rust:stable:
# https://hub.docker.com/_/rust/
image: "rust"
<<: *cargo_test
rust:nightly:
.rust:nightly:
# https://hub.docker.com/r/rustlang/rust/
image: "rustlang/rust:nightly"
<<: *cargo_test
@@ -49,30 +55,30 @@ rust:nightly:
- web
flatpak:
image: registry.gitlab.com/alatiera/gnome-nightly-oci/rust-bundle:latest
image: registry.gitlab.gnome.org/gnome/gnome-runtime-images/rust_bundle:3.28
stage: test
script:
- flatpak-builder --stop-at=hammond app org.gnome.Hammond.json
# https://gitlab.gnome.org/World/hammond/issues/55
- flatpak-builder --stop-at=gnome-podcasts app org.gnome.Podcasts.json
# https://gitlab.gnome.org/World/podcasts/issues/55
# Force regeneration of gresources regardless of artifacts chage
- flatpak-builder --run app org.gnome.Hammond.json glib-compile-resources --sourcedir=hammond-gtk/resources/ hammond-gtk/resources/resources.xml
- flatpak-builder --run app org.gnome.Podcasts.json glib-compile-resources --sourcedir=podcasts-gtk/resources/ podcasts-gtk/resources/resources.xml
# Build the flatpak repo
- flatpak-builder --run app org.gnome.Hammond.json meson --prefix=/app --libdir=/app/lib _build
- flatpak-builder --run app org.gnome.Hammond.json ninja -C _build install
- flatpak-builder --finish-only app org.gnome.Hammond.json
- flatpak-builder --run app org.gnome.Podcasts.json meson --prefix=/app --libdir=/app/lib _build
- flatpak-builder --run app org.gnome.Podcasts.json ninja -C _build install
- flatpak-builder --finish-only app org.gnome.Podcasts.json
- flatpak build-export repo app
# Create a flatpak bundle
- flatpak build-bundle repo hammond-dev.flatpak org.gnome.Hammond
- flatpak build-bundle repo ${BUNDLE} org.gnome.Podcasts
# Run the tests
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1
# - flatpak-builder --run app org.gnome.Hammond.json cargo test -- --test-threads=1 --ignored
# - flatpak-builder --run app org.gnome.Podcasts.json cargo test -- --test-threads=1
# - flatpak-builder --run app org.gnome.Podcasts.json cargo test -- --test-threads=1 --ignored
artifacts:
paths:
- hammond-dev.flatpak
expire_in: 2 days
- $BUNDLE
expire_in: 30 days
cache:
# JOB_NAME - Each job will have it's own cache
@@ -83,6 +89,34 @@ flatpak:
- .flatpak-builder/cache/
- target/
review:
stage: review
dependencies:
- flatpak
script:
- echo "Generating flatpak deployment"
artifacts:
paths:
- $BUNDLE
expire_in: 30 days
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://gitlab.gnome.org/$CI_PROJECT_PATH/-/jobs/$CI_JOB_ID/artifacts/raw/${BUNDLE}
on_stop: stop_review
except:
- master@World/podcasts
stop_review:
stage: review
script:
- echo "Stopping flatpak deployment"
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
except:
- master@World/podcasts
# Configure and run rustfmt on nightly
# Exits and builds fails if on bad format
rustfmt:
@@ -91,22 +125,20 @@ rustfmt:
script:
- rustc -Vv && cargo -Vv
- cargo fmt --version
- cargo fmt --all -- --write-mode=diff
- cargo fmt --all -- --check
# Configure and run clippy on nightly
# Only fails on errors atm.
clippy:
image: "rustlang/rust:nightly"
.clippy:
image: "registry.gitlab.gnome.org/alatiera/hammond-container-images/clippy:nightly"
stage: lint
variables:
RUSTFLAGS: "--cfg rayon_unstable"
before_script:
- apt-get update -yqq
- apt-get install -yqq --no-install-recommends build-essential libgtk-3-dev meson
CARGO_HOME: ".cargo_cache"
script:
- rustc --version && cargo --version
- cargo install clippy --force
- cargo clippy --version
# Force regeneration of gresources regardless of artifacts chage
- cd hammond-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cd podcasts-gtk/resources/ && glib-compile-resources --generate resources.xml && cd ../../
- cargo clippy --all
allow_failure: true
<<: *cargo_cache
+1 -1
View File
@@ -18,7 +18,7 @@ Some common cases might be:
Steps to reproduce:
1. Open Hammond
1. Open GNOME Podcasts
2. Do an action
3. ...
+39 -6
View File
@@ -1,9 +1,42 @@
Detailed description of the issue. Put as much information as you can, potentially
with images showing the issue.
# Steps to reproduce
<!--
Explain in detail the steps on how the issue can be reproduced.
-->
1.
2.
3.
Steps to reproduce:
Reproducible in:
<!--
Please test if the issue was already fixed in the unstable version of the app.
For that, follow these steps:
1. Make sure Flatpak is installed or install it following these steps https://flatpak.org/setup
2. Install the unstable version of the app following this link
https://gitlab.gnome.org/GNOME/gnome-apps-nightly/raw/master/nautilus.flatpakref and save
it as a ".flatpakref" file
1. Open Hammond
2. Do an action
3. ...
If these steps failed, write in 'Other' the distribution youre using and
the version of the app.
-->
- Flatpak unstable: (yes or no) <!-- Write "yes" or "no" after the semicolon. -->
- Other:
# Current behavior
<!-- Describe the current behavior. -->
# Expected behavior
<!-- Describe the expected behavior. -->
# Additional information
<!--
Provide more information that could be relevant.
If the issue is a crash, provide a stack trace following the steps in:
https://wiki.gnome.org/Community/GettingInTouch/Bugzilla/GettingTraces
-->
<!-- Ignore the text under this line. -->
/label ~"Bug"
+41
View File
@@ -0,0 +1,41 @@
# Current problems
<!--
What are the problems that the current project has?
For example:
* User cannot use the keyboard to perform most common actions
or
* User cannot see documents from cloud services
-->
# Goals & use cases
<!--
What are the use cases that this proposal will cover? What are the end goals?
For example:
* User needs to share a file with their friends.
or
* It should be easy to edit a picture within the app.
-->
# Requirements
<!--
What does the solution needs to ensure for being succesful?
For example:
* Work on small form factors and touch
or
* Use the Meson build system and integrate with it
-->
# Relevant art
<!--
Is there any product that has implemented something similar? Put links to other
projects, pictures, links to other code, etc.
-->
# Proposal & plan
<!-- What's the solution and how should be achieved? It can be split in smaller
tasks of minimum change, so they can be delivered across several releases. -->
/label ~"Epic"
+17 -10
View File
@@ -1,17 +1,24 @@
Detailed description of the feature. Put as much information as you can.
### Use cases
<!--
Describe what problem(s) the user is experiencing and that this request
is trying to solve.
-->
Proposed Mockups:
(Add mockups of the proposed feature)
### Desired behavior
<!-- Describe the desired functionality. -->
## Design Tasks
* [ ] design tasks
### Benefits of the solution
<!-- List the possible benefits of the solution and how it fits in the project. -->
## Development Tasks
* [ ] development tasks
### Possible drawbacks
<!--
Describe possible drawbacks of the feature and list how it could affect
the project i.e. UI discoverability, complexity, impact in more or less
number of users, etc.
-->
## QA Tasks
* [ ] qa (quality assurance) tasks
<!-- Ignore the text under this line. -->
/label ~"Feature"
+94 -12
View File
@@ -5,7 +5,89 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added:
### Changed:
### Fixed:
### Removed:
## [0.4.3] - 2018-07-27
### Fixed:
- Fix the cargo vendor config for the tarball releash script. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
## [0.4.2] - 2018-07-27
### Changed:
- Minimum size requested by the Views. (Jorda Petridis) 7c96152f3f53f271247230dccf1c9cd5947b685f
### Fixed:
- Screenshot metadata in appstream data. (Jorda Petridis) a2440c19e11ca4dcdbcb67cd85259a41fe3754d6
## [0.4.1] - 2018-07-26
### Added:
- Custom icons for the fast-forward and rewind actions in the Player were added. (Tobias Bernard) e77000076b3d78b8625f4c7ef367376d0130ece6
- Hicolor and symbolic icons for the Application. (Tobias Bernard and Sam Hewitt) edae1b04801dba9d91d5d4145db79b287f0eec2c
- Basic prefferences dialog (Zander Brown). [34](https://gitlab.gnome.org/World/podcasts/merge_requests/34)
- Dbus service preperation. Not used till the MPRIS2 integration has landed. (Zander Brown) [42](https://gitlab.gnome.org/World/podcasts/merge_requests/42)
- Episodes and Images will only get drawn when needed. Big Performance impact. (Jordan Petridis) [43](https://gitlab.gnome.org/World/podcasts/merge_requests/43)
### Changed:
- The `ShowWidget` control button were moved to a secondary menu in the Headerbar. (Jordan Petridis) 536805791e336a3e112799be554706bb804d2bef
- EmptyView layout improvements. (Jorda Petridis) 3c3d6c1e7f15b88308a9054b15a6ca0d8fa233ce 518ea9c8b57885c44bda9c418b19fef26ae0e55d
- Improved the `AddButton` behavior. (Jorda Petridis) 67ab54f8203f19aad198dc49e935127d25432b41
### Fixed:
- A couple reffence cycles where fixed. (Jorda Petridis)
### Removed:
- The delay between the application startup and the `update_on_startup` action. (Jorda Petridis) 7569465a612ee5ef84d0e58f4e1010c8d14080d4
## [0.4.0] - 2018-07-04
### Added:
- Keyboard Shortcuts and a Shortcuts dialog were implemented. (ZanderBrown)
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
### Changed:
- The `FileChooser` of the OPML import was changed to use the `FileChooserNative` widget/API. (ZanderBrown)
[!33](https://gitlab.gnome.org/World/podcasts/merge_requests/33)
- The `EpisdeWidget` was refactored.
[!38](https://gitlab.gnome.org/World/podcasts/merge_requests/38)
- `EpisdeWidget`'s progressbar was changed to be non-blocking and should feel way more responsive now. 9b0ac5b83dadecdff51cd398293afdf0d5276012
- An embeded audio player was implemented!
[!40](https://gitlab.gnome.org/World/podcasts/merge_requests/40)
- Various Database changes.
[!41](https://gitlab.gnome.org/World/podcasts/merge_requests/41)
### Fixed:
- Fixed a bug whre the about dialog would be unclosable. (ZanderBrown) [!37](https://gitlab.gnome.org/World/podcasts/merge_requests/37)
## [0.3.4] - 2018-05-20
### Fixed:
- Flatpak can now access the Home folder. This fixes the OPML import feature from
not being able to access any file.
## [0.3.3] - 2018-05-19
### Added:
- Initial functionality for importing shows from an OPML file was implemented.
- ShowsView now rembmers the vertical alignment of the scrollbar between refreshes. 4d2b64e79d8518454b3677612664cd32044cf837
### Changed:
- Minimum `rustc` version requirment was bumped to `1.26`
- Some animations should be smoother now. 7d598bb1d08b05fd5ab532657acdad967c0afbc3
- InAppNotification now can be used to propagate some erros to the user. 7035fe05c4741b3e7ccce6827f72766226d5fc0a and 118dac5a1ab79c0b4ebe78e88256a4a38b138c04
### Fixed:
- Fixed a of by one bug in the `ShowsView` where the last show was never shown. bd12b09cbc8132fd39a266fd091e24bc6c3c040f
## [0.3.2] - 2018-05-07
### Added:
@@ -13,9 +95,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
### Changed:
- Downlaoding and loading images now is done asynchronously and is not blocking programs execution.
[#7](https://gitlab.gnome.org/World/hammond/issues/7)
[#7](https://gitlab.gnome.org/World/podcasts/issues/7)
- Bold, italics links and some other `html` tags can now be rendered in the Show Description.
[#25](https://gitlab.gnome.org/World/hammond/issues/25)
[#25](https://gitlab.gnome.org/World/podcasts/issues/25)
- `Rayon` Threadpools are now used instead of unlimited one-off threads.
- `EpisdeWidget`s are now loaded asynchronously accross views.
- `EpisodeWidget`s no longer trigger a `View` refresh for trivial stuff 03bd95184808ccab3e0ea0e3713a52ee6b7c9ab4
@@ -23,7 +105,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `ShowWidget` Description is inside a scrolled window now
### Fixed:
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/hammond/issues/57)
- `EpisodeWidget` Height now is consistent accros views [#57](https://gitlab.gnome.org/World/podcasts/issues/57)
- Implemented a tail-recursion loop to follow-up when a feed redirects to another url. c6a24e839a8ba77d09673f299cfc1e64ba7078f3
### Removed:
@@ -32,27 +114,27 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [0.3.1] - 2018-03-28
### Added:
- Ability to mark all episodes of a Show as watched.
[#47](https://gitlab.gnome.org/World/hammond/issues/47)
[#47](https://gitlab.gnome.org/World/podcasts/issues/47)
- Now you are able to subscribe to itunes™ podcasts by using the itunes link of the show.
[#49](https://gitlab.gnome.org/World/hammond/issues/49)
[#49](https://gitlab.gnome.org/World/podcasts/issues/49)
- Hammond now remembers the window size and position. (Rowan Lewis)
[#50](https://gitlab.gnome.org/World/hammond/issues/50)
[#50](https://gitlab.gnome.org/World/podcasts/issues/50)
- Implemnted the initial work for integrating with GSettings and storing preferences. (Rowan Lewis)
[!22](https://gitlab.gnome.org/World/hammond/merge_requests/22) [!23](https://gitlab.gnome.org/World/hammond/merge_requests/23)
[!22](https://gitlab.gnome.org/World/podcasts/merge_requests/22) [!23](https://gitlab.gnome.org/World/podcasts/merge_requests/23)
- Shows without episodes now display an empty message similar to EmptyView.
[#44](https://gitlab.gnome.org/World/hammond/issues/44)
[#44](https://gitlab.gnome.org/World/podcasts/issues/44)
### Changed:
- EpisdeWidget has been reimplemented as a compile time state machine.
[!18](https://gitlab.gnome.org/World/hammond/merge_requests/18)
[!18](https://gitlab.gnome.org/World/podcasts/merge_requests/18)
- Content Views no longer scroll horizontally when shrunk bellow their minimum size.
[#35](https://gitlab.gnome.org/World/hammond/issues/35)
[#35](https://gitlab.gnome.org/World/podcasts/issues/35)
- Some requests now use the Tor Browser's user agent. (Rowan Lewis)
[#53](https://gitlab.gnome.org/World/hammond/issues/53)
[#53](https://gitlab.gnome.org/World/podcasts/issues/53)
### Fixed:
- Double border aroun the main window was fixed. (Rowan Lewis)
[#52](https://gitlab.gnome.org/World/hammond/issues/52)
[#52](https://gitlab.gnome.org/World/podcasts/issues/52)
## [0.3.0] - 2018-02-11
- Tobias Bernard Redesigned the whole Gtk+ client.
+8 -8
View File
@@ -1,18 +1,18 @@
## Contributing to Hammond
## Contributing to GNOME Podcasts
Thank you for looking in this file!
When contributing to the development of Hammond, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
When contributing to the development of GNOME Podcasts, please first discuss the change you wish to make via issue, email, or any other method with the maintainers before making a change.
If you have any questions regarding the use or development of Hammond,
If you have any questions regarding the use or development of GNOME Podcasts,
want to discuss design or simply hang out, please join us in [#hammond on irc.gnome.org.](irc://irc.gnome.org/#hammond)
Please note we have a [code of conduct](https://wiki.gnome.org/Foundation/CodeOfConduc), please follow it in all your interactions with the project.
## Source repository
Hammond's main source repository is at gitlab.gnome.org. You can view
the web interface [here](https://gitlab.gnome.org/World/hammond)
GNOME Podcasts's main source repository is at gitlab.gnome.org. You can view
the web interface [here](https://gitlab.gnome.org/World/podcasts)
Development happens in the master branch.
@@ -50,8 +50,8 @@ In order to run the test suite use the following: `cargo test -- --test-threads=
# Issues, issues and more issues!
There are many ways you can contribute to Hammond, and all of them involve creating issues
in [Hammond issue tracker](https://gitlab.gnome.org/World/hammond/issues). This is the entry point for your contribution.
There are many ways you can contribute to GNOME Podcasts, and all of them involve creating issues
in [GNOME Podcasts issue tracker](https://gitlab.gnome.org/World/podcasts/issues). This is the entry point for your contribution.
To create an effective and high quality ticket, try to put the following information on your
ticket:
@@ -74,7 +74,7 @@ If it's an issue, add the steps to reproduce like this:
Steps to reproduce:
1. Open Hammond
1. Open GNOME Podcasts
2. Do an Action
3. ...
Generated
+1097 -658
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -1,8 +1,8 @@
[workspace]
members = [
"hammond-data",
"hammond-downloader",
"hammond-gtk"
"podcasts-data",
"podcasts-downloader",
"podcasts-gtk"
]
[profile.release]
+2 -2
View File
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Hammond
GNOME Podcasts
Copyright (C) 2017 Jordan Petridis
This program is free software: you can redistribute it and/or modify
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
Hammond Copyright (C) 2017 Jordan Petridis
GNOME Podcasts Copyright (C) 2017 Jordan Petridis
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
+38 -29
View File
@@ -1,25 +1,26 @@
# Hammond
# GNOME Podcasts
### A Podcast Client for GNOME written in Rust.
### A Podcast application for GNOME.
Listen to your favorite podcasts, right from your desktop.
![episdes_view](./screenshots/episodes_view.png)
![episdes_view](./screenshots/home_view.png)
![shows_view](./screenshots/shows_view.png)
![show_widget](./screenshots/show_widget.png)
## Quick start
Hammond can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
GNOME Podcasts can be built and run with [Gnome Builder](https://wiki.gnome.org/Apps/Builder) >= 3.28.
Get Builder [here](https://wiki.gnome.org/Apps/Builder/Downloads)
## Broken Feeds
Found a feed that does not work in Hammond?
Please [open an issue](https://gitlab.gnome.org/World/hammond/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
Found a feed that does not work in GNOME Podcasts?
Please [open an issue](https://gitlab.gnome.org/World/podcasts/issues/new) and choose the `BrokenFeed` template so we will know and fix it!
## Getting in Touch
If you have any questions regarding the use or development of Hammond,
If you have any questions regarding the use or development of GNOME Podcasts,
want to discuss design or simply hang out, please join us in `#hammond` on
[irc.gnome.org.][irc] or [matrix][matrix].
@@ -31,11 +32,11 @@ There isn't much documentation yet, so you will probably have question about par
### Flatpak
Flatpak is the reccomended way of building and installing Hammond.
Flatpak is the reccomended way of building and installing GNOME Podcasts.
#### Building a Flatpak
Download the `org.gnome.Hammond.json` flatpak manifest from this repo.
Download the `org.gnome.Podcasts.json` flatpak manifest from this repo.
```bash
# Add flathub repo
@@ -46,21 +47,21 @@ flatpak --user remote-add gnome-nightly --if-not-exists https://sdk.gnome.org/gn
flatpak --user install gnome-nightly org.gnome.Sdk org.gnome.Platform
# Install the required rust-stable extension from flathub
flatpak --user install flathub org.freedesktop.Sdk.Extension.rust-stable
flatpak-builder --user --repo=repo hammond org.gnome.Hammond.json --force-clean
flatpak-builder --user --repo=repo podcasts org.gnome.Podcasts.json --force-clean
```
To install the resulting flatpak you can do:
```bash
flatpak build-bundle repo hammond.flatpak org.gnome.Hammond
flatpak install --user --bundle hammond.flatpak
flatpak build-bundle repo gnome-podcasts.flatpak org.gnome.Podcasts
flatpak install --user --bundle gnome-podcasts.flatpak
```
### Building from soure
```sh
git clone https://gitlab.gnome.org/World/hammond.git
cd hammond/
git clone https://gitlab.gnome.org/World/podcasts.git
cd gnome-podcasts/
meson --prefix=/usr build
ninja -C build
sudo ninja -C build install
@@ -68,8 +69,9 @@ sudo ninja -C build install
#### Dependencies
* Rust stable 1.22 or later along with cargo.
* Rust stable 1.27 or later along with cargo.
* Gtk+ 3.22 or later
* Gstreamer 1.12 or later
* Meson
* A network connection
@@ -78,15 +80,23 @@ Offline build are possible too, but [`cargo-vendor`][vendor] would have to be se
**Debian/Ubuntu**
```sh
apt-get update -yqq
apt-get install -yqq --no-install-recommends build-essential
apt-get install -yqq --no-install-recommends rustc cargo libgtk-3-dev meson
apt-get install -yqq cargo rustc libsqlite3-dev libssl-dev libgtk-3-dev meson \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav libgstrtspserver-1.0-dev ibgstreamer-plugins-bad1.0-dev
```
**Fedora**
```sh
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson
dnf install -y rust cargo gtk3-devel glib2-devel openssl-devel sqlite-devel meson \
gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-devel-docs \
gstreamer1-plugins-base-devel gstreamer1-plugins-base-devel-docs \
gstreamer1-plugins-good gstreamer1-plugins-good-extras \
gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-devel \
gstreamer1-plugins-bad-free-extras
```
If you happen to build it on other distributions please let me know the names
@@ -98,11 +108,11 @@ There alot of thins yet to be done.
If you want to contribute, please check the [Contributions Guidelines][contribution-guidelines].
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/hammond/issues) or by opening a [New issue](https://gitlab.gnome.org/World/hammond/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
You can start by taking a look at [Issues](https://gitlab.gnome.org/World/podcasts/issues) or by opening a [New issue](https://gitlab.gnome.org/World/podcasts/issues/new?issue%5Bassignee_id%5D=&issue%5Bmilestone_id%5D=).
There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source code.
[contribution-guidelines]: https://gitlab.gnome.org/World/hammond/blob/master/CONTRIBUTING.md
[contribution-guidelines]: https://gitlab.gnome.org/World/podcasts/blob/master/CONTRIBUTING.md
## Overview
@@ -110,15 +120,15 @@ There are also some minor tasks tagged with `TODO:` and `FIXME:` in the source c
```sh
$ tree -d
├── screenshots # png's used in the README.md
├── hammond-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
├── podcasts-data # Storate related stuff, SQLite, XDG setup, RSS Parser.
│   ├── migrations # Diesel SQL migrations.
│   │   └── ...
│   ├── src
│   └── tests
│   └── feeds # Raw RSS Feeds used for tests.
├── hammond-downloader # Really basic, Really crappy downloader.
├── podcasts-downloader # Really basic, Really crappy downloader.
│   └── src
├── hammond-gtk # The Gtk+ Client
├── podcasts-gtk # The Gtk+ Client
│   ├── resources # GResources folder
│   │   └── gtk # Contains the glade.ui files.
│   └── src
@@ -128,13 +138,12 @@ $ tree -d
## A note about the project's name
The project was named after Allan Moore's character [Evey Hammond](https://en.wikipedia.org/wiki/Evey_Hammond) from the graphic novel V for Vendetta.
It has nothing to do with the horrible headlines on the news.
The project used to be called Hammond, after Allan Moore's character [Evey Hammond](https://en.wikipedia.org/wiki/Evey_Hammond) from the graphic novel V for Vendetta.
It was renamed to GNOME Podcasts on 2018/07/24 shortly before its first public release.
## Acknowledgments
Hammond's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Design/Apps/Music) and [Vocal](http://vocalproject.net/).
GNOME Podcasts's design is heavily insired by [GNOME Music](https://wiki.gnome.org/Design/Apps/Music) and [Vocal](http://vocalproject.net/).
We also copied some elements from [GNOME News](https://wiki.gnome.org/Design/Apps/Potential/News).
@@ -142,4 +151,4 @@ And almost the entirety of the build system is copied from the [Fractal](https:/
[vendor]: https://github.com/alexcrichton/cargo-vendor
[irc]: irc://irc.gnome.org/#hammond
[matrix]: https://matrix.to/#/#hammond:matrix.org
[matrix]: https://matrix.to/#/#gnome-podcasts:matrix.org
+2 -9
View File
@@ -4,12 +4,11 @@
## Priorities
- [ ] Unplayed Only and Downloaded only view.
- [ ] OPML import/export // Probably need to create a crate.
## Second
- [ ] Make use of file metadas, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] Make use of file metadas?, [This](https://github.com/GuillaumeGomez/audio-video-metadata) might be helpfull.
- [ ] Episode queue
- [ ] Embedded player
- [ ] MPRIS integration
@@ -19,14 +18,8 @@
- [ ] Download Queue
- [ ] Ability to Stream content on demand
- [ ] soundcloud and itunes feeds // [This](http://getrssfeed.com) seems intresting.
- [ ] rss feeds from soundcloud urls? // [This](http://getrssfeed.com) seems intresting.
- [ ] Integrate with Itunes API for various crap?
- [ ] YoutubeFeeds?
## Rest Tasks
**Would be nice:**
- [ ] Make Podcast cover fetchng and loading not block the execution of the program at startup.
- [ ] Lazy evaluate episode loading based on the show_widget's scrolling.
-113
View File
@@ -1,113 +0,0 @@
#![recursion_limit = "1024"]
#![cfg_attr(all(test, feature = "clippy"), allow(option_unwrap_used, result_unwrap_used))]
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
#![cfg_attr(
feature = "clippy",
warn(
option_unwrap_used, result_unwrap_used, print_stdout, wrong_pub_self_convention, mut_mut,
non_ascii_literal, similar_names, unicode_not_nfc, enum_glob_use, if_not_else,
items_after_statements, used_underscore_binding
)
)]
#![allow(unknown_lints)]
#![warn(
bad_style, const_err, dead_code, improper_ctypes, legacy_directory_ownership,
non_shorthand_field_patterns, no_mangle_generic_items, overflowing_literals, path_statements,
patterns_in_fns_without_body, plugin_as_library, private_in_public, private_no_mangle_fns,
private_no_mangle_statics, safe_extern_statics, unconditional_recursion,
unions_with_drop_fields, unused_allocation, unused_comparisons, unused_parens, while_true
)]
#![warn(
missing_debug_implementations, missing_docs, trivial_casts, trivial_numeric_casts,
unused_extern_crates, unused
)]
#![deny(warnings)]
//! FIXME: Docs
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[macro_use]
extern crate derive_builder;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
// #[macro_use]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate ammonia;
extern crate chrono;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate native_tls;
extern crate num_cpus;
extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate tokio_core;
extern crate url;
extern crate xdg;
pub mod database;
#[allow(missing_docs)]
pub mod dbqueries;
#[allow(missing_docs)]
pub mod errors;
mod feed;
pub(crate) mod models;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils;
pub use feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetQuery, Podcast, PodcastCoverQuery, Source};
// Set the user agent, See #53 for more
// Keep this in sync with Tor-browser releases
const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)]
pub mod xdg_dirs {
use std::path::PathBuf;
use xdg;
lazy_static!{
pub(crate) static ref HAMMOND_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("hammond").unwrap()
};
/// XDG_DATA Directory `Pathbuf`.
pub static ref HAMMOND_DATA: PathBuf = {
HAMMOND_XDG.create_data_directory(HAMMOND_XDG.get_data_home()).unwrap()
};
/// XDG_CONFIG Directory `Pathbuf`.
pub static ref HAMMOND_CONFIG: PathBuf = {
HAMMOND_XDG.create_config_directory(HAMMOND_XDG.get_config_home()).unwrap()
};
/// XDG_CACHE Directory `Pathbuf`.
pub static ref HAMMOND_CACHE: PathBuf = {
HAMMOND_XDG.create_cache_directory(HAMMOND_XDG.get_cache_home()).unwrap()
};
/// Hammond Download Direcotry `PathBuf`.
pub static ref DL_DIR: PathBuf = {
HAMMOND_XDG.create_data_directory("Downloads").unwrap()
};
}
}
-25
View File
@@ -1,25 +0,0 @@
#![recursion_limit = "1024"]
#![warn(unused_extern_crates, unused)]
#![allow(unknown_lints)]
#![cfg_attr(feature = "cargo-clippy", allow(blacklisted_name, option_map_unit_fn))]
#![deny(warnings)]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate glob;
extern crate hammond_data;
extern crate hyper;
extern crate mime_guess;
extern crate reqwest;
extern crate tempdir;
pub mod downloader;
pub mod errors;
-91
View File
@@ -1,91 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond 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.
Hammond 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 Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="empty_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="pixel_size">128</property>
<property name="icon_name">application-rss+xml-symbolic</property>
<property name="use_fallback">True</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">No Feed Subscription Found</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.4399999999999999"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">You can subscribe to feeds using the "+" button</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</interface>
-6
View File
@@ -1,6 +0,0 @@
# subdir('icons')
install_data('org.gnome.Hammond.desktop', install_dir : datadir + '/applications')
install_data('org.gnome.Hammond.appdata.xml', install_dir : datadir + '/appdata')
install_data('org.gnome.Hammond.gschema.xml', install_dir: join_paths(datadir, 'glib-2.0', 'schemas'))
meson.add_install_script('../../scripts/compile-gschema.py')
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>org.gnome.Hammond</id>
<name>Hammond</name>
<project_license>GPL-3.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Jordan Petridis</developer_name>
<summary>Modern Podcast Client for the GNOME desktop</summary>
<description xml:lang="en">
<p>
Hammond is a modern, reliable, and fast Podcast Client for the GNOME
desktop written in Rust.
</p>
</description>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<screenshots>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/episodes_view.png</image>
<caption>Page 1</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/shows_view.png</image>
<caption>Page 2</caption>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/hammond/raw/master/screenshots/show_widget.png</image>
<caption>Page 3</caption>
</screenshot>
</screenshots>
<releases>
<release version="0.3.2" date="2018-05-07"/>
</releases>
<url type="homepage">https://gitlab.gnome.org/World/hammond</url>
<update_contact>jpetridis@gnome.org</update_contact>
<developer_name>Jordan Petridis and others</developer_name>
</component>
@@ -1,11 +0,0 @@
[Desktop Entry]
Name=Hammond
GenericName=Podcast Client
Comment=Play, Subscribe and Manage Podcast Feeds.
Icon=multimedia-player
Exec=hammond
Terminal=false
Type=Application
StartupNotify=true
Categories=AudioVideo;Audio;Video;
Keywords=Podcast
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/hammond/">
<file compressed="true" preprocess="xml-stripblanks">gtk/episode_widget.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/show_widget.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/empty_show.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/episodes_view_widget.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_view.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/shows_child.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/headerbar.ui</file>
<file compressed="true" preprocess="xml-stripblanks">gtk/inapp_notif.ui</file>
<file compressed="true">gtk/style.css</file>
</gresource>
</gresources>
-223
View File
@@ -1,223 +0,0 @@
#![allow(new_without_default)]
use gio::{ApplicationExt, ApplicationExtManual, ApplicationFlags, Settings, SettingsExt};
use glib;
use gtk;
use gtk::prelude::*;
use gtk::SettingsExt as GtkSettingsExt;
use hammond_data::Podcast;
use headerbar::Header;
use settings::{self, WindowGeometry};
use stacks::{Content, PopulatedState};
use utils;
use widgets::{mark_all_notif, remove_show_notif};
use std::rc::Rc;
use std::sync::mpsc::{channel, Receiver, Sender};
use std::sync::Arc;
#[derive(Clone, Debug)]
pub enum Action {
RefreshAllViews,
RefreshEpisodesView,
RefreshEpisodesViewBGR,
RefreshShowsView,
ReplaceWidget(Arc<Podcast>),
RefreshWidgetIfSame(i32),
ShowWidgetAnimated,
ShowShowsAnimated,
HeaderBarShowTile(String),
HeaderBarNormal,
HeaderBarShowUpdateIndicator,
HeaderBarHideUpdateIndicator,
MarkAllPlayerNotification(Arc<Podcast>),
RemoveShow(Arc<Podcast>),
}
#[derive(Debug)]
pub struct App {
app_instance: gtk::Application,
window: gtk::Window,
overlay: gtk::Overlay,
header: Rc<Header>,
content: Rc<Content>,
receiver: Receiver<Action>,
sender: Sender<Action>,
settings: Settings,
}
impl App {
pub fn new() -> App {
let settings = Settings::new("org.gnome.Hammond");
let application = gtk::Application::new("org.gnome.Hammond", ApplicationFlags::empty())
.expect("Application Initialization failed...");
// Weird magic I copy-pasted that sets the Application Name in the Shell.
glib::set_application_name("Hammond");
glib::set_prgname(Some("Hammond"));
let cleanup_date = settings::get_cleanup_date(&settings);
utils::cleanup(cleanup_date);
// Create the main window
let window = gtk::Window::new(gtk::WindowType::Toplevel);
window.set_title("Hammond");
let app_clone = application.clone();
let window_clone = window.clone();
let settings_clone = settings.clone();
window.connect_delete_event(move |_, _| {
WindowGeometry::from_window(&window_clone).write(&settings_clone);
app_clone.quit();
Inhibit(false)
});
let (sender, receiver) = channel();
// Create a content instance
let content =
Rc::new(Content::new(sender.clone()).expect("Content Initialization failed."));
// Create the headerbar
let header = Rc::new(Header::new(&content, &window, &sender));
// Add the content main stack to the overlay.
let overlay = gtk::Overlay::new();
overlay.add(&content.get_stack());
// Add the overlay to the main window
window.add(&overlay);
App {
app_instance: application,
window,
overlay,
header,
content,
receiver,
sender,
settings,
}
}
fn setup_timed_callbacks(&self) {
self.setup_dark_theme();
self.setup_refresh_on_startup();
self.setup_auto_refresh();
}
fn setup_dark_theme(&self) {
let settings = gtk::Settings::get_default().unwrap();
let enabled = self.settings.get_boolean("dark-theme");
settings.set_property_gtk_application_prefer_dark_theme(enabled);
}
fn setup_refresh_on_startup(&self) {
// Update the feeds right after the Application is initialized.
if self.settings.get_boolean("refresh-on-startup") {
let sender = self.sender.clone();
info!("Refresh on startup.");
// The ui loads async, after initialization
// so we need to delay this a bit so it won't block
// requests that will come from loading the gui on startup.
gtk::timeout_add(1500, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(false)
});
}
}
fn setup_auto_refresh(&self) {
let refresh_interval = settings::get_refresh_interval(&self.settings).num_seconds() as u32;
let sender = self.sender.clone();
info!("Auto-refresh every {:?} seconds.", refresh_interval);
gtk::timeout_add_seconds(refresh_interval, move || {
let s: Option<Vec<_>> = None;
utils::refresh(s, sender.clone());
glib::Continue(true)
});
}
#[inline]
pub fn run(self) {
WindowGeometry::from_settings(&self.settings).apply(&self.window);
let window = self.window.clone();
self.app_instance.connect_startup(move |app| {
build_ui(&window, app);
});
self.setup_timed_callbacks();
let content = self.content.clone();
let headerbar = self.header.clone();
let sender = self.sender.clone();
let overlay = self.overlay.clone();
let receiver = self.receiver;
gtk::timeout_add(50, move || {
match receiver.try_recv() {
Ok(Action::RefreshAllViews) => content.update(),
Ok(Action::RefreshShowsView) => content.update_shows_view(),
Ok(Action::RefreshWidgetIfSame(id)) => content.update_widget_if_same(id),
Ok(Action::RefreshEpisodesView) => content.update_home(),
Ok(Action::RefreshEpisodesViewBGR) => content.update_home_if_background(),
Ok(Action::ReplaceWidget(pd)) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.replace_widget(pd.clone())
.map_err(|err| error!("Failed to update ShowWidget: {}", err))
.map_err(|_| error!("Failed ot update ShowWidget {}", pd.title()))
.ok();
}
Ok(Action::ShowWidgetAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut().switch_visible(
PopulatedState::Widget,
gtk::StackTransitionType::SlideLeft,
);
}
Ok(Action::ShowShowsAnimated) => {
let shows = content.get_shows();
let mut pop = shows.borrow().populated();
pop.borrow_mut()
.switch_visible(PopulatedState::View, gtk::StackTransitionType::SlideRight);
}
Ok(Action::HeaderBarShowTile(title)) => headerbar.switch_to_back(&title),
Ok(Action::HeaderBarNormal) => headerbar.switch_to_normal(),
Ok(Action::HeaderBarShowUpdateIndicator) => headerbar.show_update_notification(),
Ok(Action::HeaderBarHideUpdateIndicator) => headerbar.hide_update_notification(),
Ok(Action::MarkAllPlayerNotification(pd)) => {
let notif = mark_all_notif(pd, &sender);
notif.show(&overlay);
}
Ok(Action::RemoveShow(pd)) => {
let notif = remove_show_notif(pd, sender.clone());
notif.show(&overlay);
}
Err(_) => (),
}
Continue(true)
});
ApplicationExtManual::run(&self.app_instance, &[]);
}
}
fn build_ui(window: &gtk::Window, app: &gtk::Application) {
window.set_application(app);
window.show_all();
window.activate();
app.connect_activate(move |_| ());
}
-257
View File
@@ -1,257 +0,0 @@
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use failure::ResultExt;
use url::Url;
use hammond_data::dbqueries;
use hammond_data::Source;
use std::sync::mpsc::Sender;
use app::Action;
use stacks::Content;
use utils::{itunes_to_rss, refresh};
#[derive(Debug, Clone)]
pub struct Header {
container: gtk::HeaderBar,
add_toggle: gtk::MenuButton,
switch: gtk::StackSwitcher,
back_button: gtk::Button,
show_title: gtk::Label,
about_button: gtk::ModelButton,
update_button: gtk::ModelButton,
update_box: gtk::Box,
update_label: gtk::Label,
update_spinner: gtk::Spinner,
}
impl Default for Header {
fn default() -> Header {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let header: gtk::HeaderBar = builder.get_object("headerbar").unwrap();
let add_toggle: gtk::MenuButton = builder.get_object("add_toggle").unwrap();
let switch: gtk::StackSwitcher = builder.get_object("switch").unwrap();
let back_button: gtk::Button = builder.get_object("back_button").unwrap();
let show_title: gtk::Label = builder.get_object("show_title").unwrap();
let update_button: gtk::ModelButton = builder.get_object("update_button").unwrap();
let update_box: gtk::Box = builder.get_object("update_notification").unwrap();
let update_label: gtk::Label = builder.get_object("update_label").unwrap();
let update_spinner: gtk::Spinner = builder.get_object("update_spinner").unwrap();
let about_button: gtk::ModelButton = builder.get_object("about_button").unwrap();
Header {
container: header,
add_toggle,
switch,
back_button,
show_title,
about_button,
update_button,
update_box,
update_label,
update_spinner,
}
}
}
// TODO: Refactor components into smaller state machines
impl Header {
pub fn new(content: &Content, window: &gtk::Window, sender: &Sender<Action>) -> Header {
let h = Header::default();
h.init(content, window, &sender);
h
}
pub fn init(&self, content: &Content, window: &gtk::Window, sender: &Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/headerbar.ui");
let add_popover: gtk::Popover = builder.get_object("add_popover").unwrap();
let new_url: gtk::Entry = builder.get_object("new_url").unwrap();
let add_button: gtk::Button = builder.get_object("add_button").unwrap();
let result_label: gtk::Label = builder.get_object("result_label").unwrap();
self.switch.set_stack(&content.get_stack());
new_url.connect_changed(clone!(add_button => move |url| {
on_url_change(url, &result_label, &add_button)
.map_err(|err| error!("Error: {}", err))
.ok();
}));
add_button.connect_clicked(clone!(add_popover, new_url, sender => move |_| {
on_add_bttn_clicked(&new_url, sender.clone())
.map_err(|err| error!("Error: {}", err))
.ok();
add_popover.hide();
}));
self.add_toggle.set_popover(&add_popover);
self.update_button
.connect_clicked(clone!(sender => move |_| {
gtk::idle_add(clone!(sender => move || {
let s: Option<Vec<_>> = None;
refresh(s, sender.clone());
glib::Continue(false)
}));
}));
self.about_button
.connect_clicked(clone!(window => move |_| {
about_dialog(&window);
}));
// Add the Headerbar to the window.
window.set_titlebar(&self.container);
let switch = &self.switch;
let add_toggle = &self.add_toggle;
let show_title = &self.show_title;
self.back_button.connect_clicked(
clone!(switch, add_toggle, show_title, sender => move |back| {
switch.show();
add_toggle.show();
back.hide();
show_title.hide();
sender.send(Action::ShowShowsAnimated)
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}),
);
}
pub fn switch_to_back(&self, title: &str) {
self.switch.hide();
self.add_toggle.hide();
self.back_button.show();
self.set_show_title(title);
self.show_title.show();
}
pub fn switch_to_normal(&self) {
self.switch.show();
self.add_toggle.show();
self.back_button.hide();
self.show_title.hide();
}
pub fn set_show_title(&self, title: &str) {
self.show_title.set_text(title)
}
pub fn show_update_notification(&self) {
self.update_spinner.start();
self.update_box.show();
self.update_spinner.show();
self.update_label.show();
}
pub fn hide_update_notification(&self) {
self.update_spinner.stop();
self.update_box.hide();
self.update_spinner.hide();
self.update_label.hide();
}
}
// FIXME: THIS ALSO SUCKS!
fn on_add_bttn_clicked(entry: &gtk::Entry, sender: Sender<Action>) -> Result<(), Error> {
let url = entry.get_text().unwrap_or_default();
let url = if url.contains("itunes.com") || url.contains("apple.com") {
info!("Detected itunes url.");
let foo = itunes_to_rss(&url)?;
info!("Resolved to {}", foo);
foo
} else {
url.to_owned()
};
let source = Source::from_url(&url).context("Failed to convert url to a Source entry.")?;
entry.set_text("");
gtk::idle_add(move || {
refresh(Some(vec![source.clone()]), sender.clone());
glib::Continue(false)
});
Ok(())
}
// FIXME: THIS SUCKS!
fn on_url_change(
entry: &gtk::Entry,
result: &gtk::Label,
add_button: &gtk::Button,
) -> Result<(), Error> {
let uri = entry
.get_text()
.ok_or_else(|| format_err!("GtkEntry blew up somehow."))?;
debug!("Url: {}", uri);
let url = Url::parse(&uri);
// TODO: refactor to avoid duplication
match url {
Ok(u) => {
if !dbqueries::source_exists(u.as_str())? {
add_button.set_sensitive(true);
result.hide();
result.set_label("");
} else {
add_button.set_sensitive(false);
result.set_label("Show already exists.");
result.show();
}
Ok(())
}
Err(err) => {
add_button.set_sensitive(false);
if !uri.is_empty() {
result.set_label("Invalid url.");
result.show();
error!("Error: {}", err);
} else {
result.hide();
}
Ok(())
}
}
}
// Totally copied it from fractal.
// https://gitlab.gnome.org/danigm/fractal/blob/503e311e22b9d7540089d735b92af8e8f93560c5/fractal-gtk/src/app.rs#L1883-1912
fn about_dialog(window: &gtk::Window) {
// Feel free to add yourself if you contribured.
let authors = &[
"Constantin Nickel",
"Gabriele Musco",
"James Wykeham-Martin",
"Jordan Petridis",
"Julian Sparber",
"Rowan Lewis",
];
let dialog = gtk::AboutDialog::new();
// Waiting for a logo.
// dialog.set_logo_icon_name("org.gnome.Hammond");
dialog.set_logo_icon_name("multimedia-player");
dialog.set_comments("Podcast Client for the GNOME Desktop.");
dialog.set_copyright("© 2017, 2018 Jordan Petridis");
dialog.set_license_type(gtk::License::Gpl30);
dialog.set_modal(true);
// TODO: make it show it fetches the commit hash from which it was built
// and the version number is kept in sync automaticly
dialog.set_version("0.3.2");
dialog.set_program_name("Hammond");
// TODO: Need a wiki page first.
// dialog.set_website("https://wiki.gnome.org/Design/Apps/Potential/Podcasts");
// dialog.set_website_label("Learn more about Hammond");
dialog.set_transient_for(window);
dialog.set_artists(&["Tobias Bernard"]);
dialog.set_authors(authors);
dialog.show();
}
-397
View File
@@ -1,397 +0,0 @@
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use humansize::FileSize;
use open;
use take_mut;
use hammond_data::dbqueries;
use hammond_data::utils::get_download_folder;
use hammond_data::EpisodeWidgetQuery;
use app::Action;
use manager;
use widgets::episode_states::*;
use std::cell::RefCell;
use std::ops::DerefMut;
use std::path::Path;
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
#[derive(Debug)]
pub struct EpisodeWidget {
pub container: gtk::Box,
date: DateMachine,
duration: DurationMachine,
title: Rc<RefCell<TitleMachine>>,
media: Rc<RefCell<MediaMachine>>,
}
impl Default for EpisodeWidget {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/episode_widget.ui");
let container: gtk::Box = builder.get_object("episode_container").unwrap();
let progress: gtk::ProgressBar = builder.get_object("progress_bar").unwrap();
let download: gtk::Button = builder.get_object("download_button").unwrap();
let play: gtk::Button = builder.get_object("play_button").unwrap();
let cancel: gtk::Button = builder.get_object("cancel_button").unwrap();
let title: gtk::Label = builder.get_object("title_label").unwrap();
let date: gtk::Label = builder.get_object("date_label").unwrap();
let duration: gtk::Label = builder.get_object("duration_label").unwrap();
let local_size: gtk::Label = builder.get_object("local_size").unwrap();
let total_size: gtk::Label = builder.get_object("total_size").unwrap();
let separator1: gtk::Label = builder.get_object("separator1").unwrap();
let separator2: gtk::Label = builder.get_object("separator2").unwrap();
let prog_separator: gtk::Label = builder.get_object("prog_separator").unwrap();
let date_machine = DateMachine::new(date, 0);
let dur_machine = DurationMachine::new(duration, separator1, None);
let title_machine = Rc::new(RefCell::new(TitleMachine::new(title, false)));
let media = MediaMachine::new(
play,
download,
progress,
cancel,
total_size,
local_size,
separator2,
prog_separator,
);
let media_machine = Rc::new(RefCell::new(media));
EpisodeWidget {
container,
title: title_machine,
duration: dur_machine,
date: date_machine,
media: media_machine,
}
}
}
impl EpisodeWidget {
#[inline]
pub fn new(episode: EpisodeWidgetQuery, sender: &Sender<Action>) -> EpisodeWidget {
let mut widget = EpisodeWidget::default();
widget.init(episode, sender);
widget
}
#[inline]
fn init(&mut self, episode: EpisodeWidgetQuery, sender: &Sender<Action>) {
// Set the date label.
self.set_date(episode.epoch());
// Set the title label state.
self.set_title(&episode);
// Set the duaration label.
self.set_duration(episode.duration());
// Determine what the state of the media widgets should be.
determine_media_state(&self.media, &episode)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
let episode = Arc::new(Mutex::new(episode));
self.connect_buttons(&episode, sender);
}
#[inline]
fn connect_buttons(&self, episode: &Arc<Mutex<EpisodeWidgetQuery>>, sender: &Sender<Action>) {
let title = self.title.clone();
if let Ok(media) = self.media.try_borrow_mut() {
media.play_connect_clicked(clone!(episode, sender => move |_| {
if let Ok(mut ep) = episode.lock() {
on_play_bttn_clicked(&mut ep, &title, &sender)
.map_err(|err| error!("Error: {}", err))
.ok();
}
}));
let media_machine = self.media.clone();
media.download_connect_clicked(clone!(media_machine, episode, sender => move |dl| {
// Make the button insensitive so it won't be pressed twice
dl.set_sensitive(false);
if let Ok(ep) = episode.lock() {
on_download_clicked(&ep, &sender)
.and_then(|_| {
info!("Donwload started succesfully.");
determine_media_state(&media_machine, &ep)
})
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
}
// Restore sensitivity after operations above complete
dl.set_sensitive(true);
}));
}
}
#[inline]
/// Determine the title state.
fn set_title(&mut self, episode: &EpisodeWidgetQuery) {
let mut machine = self.title.borrow_mut();
machine.set_title(episode.title());
take_mut::take(machine.deref_mut(), |title| {
title.determine_state(episode.played().is_some())
});
}
#[inline]
/// Set the date label depending on the current time.
fn set_date(&mut self, epoch: i32) {
let machine = &mut self.date;
take_mut::take(machine, |date| date.determine_state(i64::from(epoch)));
}
#[inline]
/// Set the duration label.
fn set_duration(&mut self, seconds: Option<i32>) {
let machine = &mut self.duration;
take_mut::take(machine, |duration| duration.determine_state(seconds));
}
}
#[inline]
fn determine_media_state(
media_machine: &Rc<RefCell<MediaMachine>>,
episode: &EpisodeWidgetQuery,
) -> Result<(), Error> {
let id = episode.rowid();
let active_dl = || -> Result<Option<_>, Error> {
let m = manager::ACTIVE_DOWNLOADS
.read()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
Ok(m.get(&id).cloned())
}()?;
let mut lock = media_machine.try_borrow_mut()?;
take_mut::take(lock.deref_mut(), |media| {
media.determine_state(
episode.length(),
active_dl.is_some(),
episode.local_uri().is_some(),
)
});
// Show or hide the play/delete/download buttons upon widget initialization.
if let Some(prog) = active_dl {
// set a callback that will update the state when the download finishes
let id = episode.rowid();
let callback = clone!(media_machine => move || {
if let Ok(guard) = manager::ACTIVE_DOWNLOADS.read() {
if !guard.contains_key(&id) {
if let Ok(ep) = dbqueries::get_episode_widget_from_rowid(id) {
determine_media_state(&media_machine, &ep)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not determine Media State"))
.ok();
return glib::Continue(false)
}
}
}
glib::Continue(true)
});
gtk::timeout_add(250, callback);
lock.cancel_connect_clicked(clone!(prog, media_machine => move |_| {
if let Ok(mut m) = prog.lock() {
m.cancel();
}
if let Ok(mut lock) = media_machine.try_borrow_mut() {
if let Ok(episode) = dbqueries::get_episode_widget_from_rowid(id) {
take_mut::take(lock.deref_mut(), |media| {
media.determine_state(
episode.length(),
false,
episode.local_uri().is_some(),
)
});
}
}
}));
drop(lock);
// Setup a callback that will update the progress bar.
update_progressbar_callback(&prog, &media_machine, id);
// Setup a callback that will update the total_size label
// with the http ContentLength header number rather than
// relying to the RSS feed.
update_total_size_callback(&prog, &media_machine);
}
Ok(())
}
#[inline]
fn on_download_clicked(ep: &EpisodeWidgetQuery, sender: &Sender<Action>) -> Result<(), Error> {
let pd = dbqueries::get_podcast_from_id(ep.podcast_id())?;
let download_fold = get_download_folder(&pd.title())?;
// Start a new download.
manager::add(ep.rowid(), download_fold)?;
// Update Views
sender.send(Action::RefreshEpisodesViewBGR)?;
Ok(())
}
#[inline]
fn on_play_bttn_clicked(
episode: &mut EpisodeWidgetQuery,
title: &Rc<RefCell<TitleMachine>>,
sender: &Sender<Action>,
) -> Result<(), Error> {
open_uri(episode.rowid())?;
episode.set_played_now()?;
let mut machine = title.try_borrow_mut()?;
take_mut::take(machine.deref_mut(), |title| {
title.determine_state(episode.played().is_some())
});
sender.send(Action::RefreshEpisodesViewBGR)?;
Ok(())
}
#[inline]
fn open_uri(rowid: i32) -> Result<(), Error> {
let uri = dbqueries::get_episode_local_uri_from_id(rowid)?
.ok_or_else(|| format_err!("Expected Some found None."))?;
if Path::new(&uri).exists() {
info!("Opening {}", uri);
open::that(&uri)?;
} else {
bail!("File \"{}\" does not exist.", uri);
}
Ok(())
}
// Setup a callback that will update the progress bar.
#[inline]
#[cfg_attr(feature = "cargo-clippy", allow(if_same_then_else))]
fn update_progressbar_callback(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
episode_rowid: i32,
) {
let callback = clone!(prog, media => move || {
progress_bar_helper(&prog, &media, episode_rowid)
.unwrap_or(glib::Continue(false))
});
timeout_add(300, callback);
}
#[inline]
#[allow(if_same_then_else)]
fn progress_bar_helper(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
episode_rowid: i32,
) -> Result<glib::Continue, Error> {
let (fraction, downloaded) = {
let m = prog.lock()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
(m.get_fraction(), m.get_downloaded())
};
// I hate floating points.
// Update the progress_bar.
if (fraction >= 0.0) && (fraction <= 1.0) && (!fraction.is_nan()) {
// Update local_size label
let size = downloaded
.file_size(SIZE_OPTS.clone())
.map_err(|err| format_err!("{}", err))?;
if let Ok(mut m) = media.try_borrow_mut() {
m.update_progress(&size, fraction);
}
}
// info!("Fraction: {}", progress_bar.get_fraction());
// info!("Fraction: {}", fraction);
// Check if the download is still active
let active = {
let m = manager::ACTIVE_DOWNLOADS
.read()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
m.contains_key(&episode_rowid)
};
if (fraction >= 1.0) && (!fraction.is_nan()) {
Ok(glib::Continue(false))
} else if !active {
Ok(glib::Continue(false))
} else {
Ok(glib::Continue(true))
}
}
// Setup a callback that will update the total_size label
// with the http ContentLength header number rather than
// relying to the RSS feed.
#[inline]
fn update_total_size_callback(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
) {
let callback = clone!(prog, media => move || {
total_size_helper(&prog, &media).unwrap_or(glib::Continue(true))
});
timeout_add(500, callback);
}
#[inline]
fn total_size_helper(
prog: &Arc<Mutex<manager::Progress>>,
media: &Rc<RefCell<MediaMachine>>,
) -> Result<glib::Continue, Error> {
// Get the total_bytes.
let total_bytes = {
let m = prog.lock()
.map_err(|_| format_err!("Failed to get a lock on the mutex."))?;
m.get_total_size()
};
debug!("Total Size: {}", total_bytes);
if total_bytes != 0 {
// Update the total_size label
if let Ok(mut m) = media.try_borrow_mut() {
take_mut::take(m.deref_mut(), |machine| {
machine.set_size(Some(total_bytes as i32))
});
}
// Do not call again the callback
Ok(glib::Continue(false))
} else {
Ok(glib::Continue(true))
}
}
// fn on_delete_bttn_clicked(episode_id: i32) -> Result<(), Error> {
// let mut ep = dbqueries::get_episode_from_rowid(episode_id)?.into();
// delete_local_content(&mut ep).map_err(From::from).map(|_| ())
// }
-936
View File
@@ -1,936 +0,0 @@
// TODO: Things that should be done.
//
// * Wherever there's a function that take 2 or more arguments of the same type,
// eg: fn new(total_size: gtk::Label, local_size: gtk::Label ..)
// Wrap the types into Struct-tuples and imple deref so it won't be possible to pass
// the wrong argument to the wrong position.
use chrono;
use glib;
use gtk;
use chrono::prelude::*;
use gtk::prelude::*;
use humansize::{file_size_opts as size_opts, FileSize};
use std::sync::Arc;
lazy_static! {
pub static ref SIZE_OPTS: Arc<size_opts::FileSizeOpts> = {
// Declare a custom humansize option struct
// See: https://docs.rs/humansize/1.0.2/humansize/file_size_opts/struct.FileSizeOpts.html
Arc::new(size_opts::FileSizeOpts {
divider: size_opts::Kilo::Binary,
units: size_opts::Kilo::Decimal,
decimal_places: 0,
decimal_zeroes: 0,
fixed_at: size_opts::FixedAt::No,
long_units: false,
space: true,
suffix: "",
allow_negative: false,
})
};
static ref NOW: DateTime<Utc> = Utc::now();
}
#[derive(Debug, Clone)]
pub struct UnInitialized;
#[derive(Debug, Clone)]
pub struct Shown;
#[derive(Debug, Clone)]
pub struct Hidden;
pub trait Visibility {}
impl Visibility for Shown {}
impl Visibility for Hidden {}
#[derive(Debug, Clone)]
pub struct Normal;
#[derive(Debug, Clone)]
pub struct GreyedOut;
#[derive(Debug, Clone)]
pub struct Title<S> {
title: gtk::Label,
state: S,
}
impl<S> Title<S> {
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn set_title(&mut self, s: &str) {
self.title.set_text(s);
}
}
impl Title<Normal> {
#[inline]
fn new(title: gtk::Label) -> Self {
Title {
title,
state: Normal {},
}
}
}
impl From<Title<Normal>> for Title<GreyedOut> {
#[inline]
fn from(f: Title<Normal>) -> Self {
f.title
.get_style_context()
.map(|c| c.add_class("dim-label"));
Title {
title: f.title,
state: GreyedOut {},
}
}
}
impl From<Title<GreyedOut>> for Title<Normal> {
#[inline]
fn from(f: Title<GreyedOut>) -> Self {
f.title
.get_style_context()
.map(|c| c.remove_class("dim-label"));
Title {
title: f.title,
state: Normal {},
}
}
}
#[derive(Debug, Clone)]
pub enum TitleMachine {
Normal(Title<Normal>),
GreyedOut(Title<GreyedOut>),
}
impl TitleMachine {
#[inline]
pub fn new(label: gtk::Label, is_played: bool) -> Self {
let m = TitleMachine::Normal(Title::<Normal>::new(label));
m.determine_state(is_played)
}
#[inline]
pub fn determine_state(self, is_played: bool) -> Self {
use self::TitleMachine::*;
match (self, is_played) {
(title @ Normal(_), false) => title,
(title @ GreyedOut(_), true) => title,
(Normal(val), true) => GreyedOut(val.into()),
(GreyedOut(val), false) => Normal(val.into()),
}
}
#[inline]
pub fn set_title(&mut self, s: &str) {
use self::TitleMachine::*;
match *self {
Normal(ref mut val) => val.set_title(s),
GreyedOut(ref mut val) => val.set_title(s),
}
}
}
#[derive(Debug, Clone)]
pub struct Usual;
#[derive(Debug, Clone)]
pub struct YearShown;
#[derive(Debug, Clone)]
pub struct Date<S> {
date: gtk::Label,
epoch: i64,
state: S,
}
impl<S> Date<S> {
#[inline]
fn into_usual(self, epoch: i64) -> Date<Usual> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b").to_string().trim());
Date {
date: self.date,
epoch: self.epoch,
state: Usual {},
}
}
#[inline]
fn into_year_shown(self, epoch: i64) -> Date<YearShown> {
let ts = Utc.timestamp(epoch, 0);
self.date.set_text(ts.format("%e %b %Y").to_string().trim());
Date {
date: self.date,
epoch: self.epoch,
state: YearShown {},
}
}
}
impl Date<UnInitialized> {
#[inline]
fn new(date: gtk::Label, epoch: i64) -> Self {
let ts = Utc.timestamp(epoch, 0);
date.set_text(ts.format("%e %b %Y").to_string().trim());
Date {
date,
epoch,
state: UnInitialized {},
}
}
}
#[derive(Debug, Clone)]
pub enum DateMachine {
UnInitialized(Date<UnInitialized>),
Usual(Date<Usual>),
WithYear(Date<YearShown>),
}
impl DateMachine {
#[inline]
pub fn new(label: gtk::Label, epoch: i64) -> Self {
let m = DateMachine::UnInitialized(Date::<UnInitialized>::new(label, epoch));
m.determine_state(epoch)
}
#[inline]
pub fn determine_state(self, epoch: i64) -> Self {
use self::DateMachine::*;
let ts = Utc.timestamp(epoch, 0);
let is_old = NOW.year() != ts.year();
match (self, is_old) {
// Into Usual
(Usual(val), false) => Usual(val.into_usual(epoch)),
(WithYear(val), false) => Usual(val.into_usual(epoch)),
(UnInitialized(val), false) => Usual(val.into_usual(epoch)),
// Into Year Shown
(Usual(val), true) => WithYear(val.into_year_shown(epoch)),
(WithYear(val), true) => WithYear(val.into_year_shown(epoch)),
(UnInitialized(val), true) => WithYear(val.into_year_shown(epoch)),
}
}
}
#[derive(Debug, Clone)]
pub struct Duration<S: Visibility> {
// TODO: make duration and separator diff types
duration: gtk::Label,
separator: gtk::Label,
state: S,
}
impl<S: Visibility> Duration<S> {
#[inline]
// This needs a better name.
// TODO: make me mut
fn set_duration(&self, minutes: i64) {
self.duration.set_text(&format!("{} min", minutes));
}
}
impl Duration<Hidden> {
#[inline]
fn new(duration: gtk::Label, separator: gtk::Label) -> Self {
duration.hide();
separator.hide();
Duration {
duration,
separator,
state: Hidden {},
}
}
}
impl From<Duration<Hidden>> for Duration<Shown> {
#[inline]
fn from(f: Duration<Hidden>) -> Self {
f.duration.show();
f.separator.show();
Duration {
duration: f.duration,
separator: f.separator,
state: Shown {},
}
}
}
impl From<Duration<Shown>> for Duration<Hidden> {
#[inline]
fn from(f: Duration<Shown>) -> Self {
f.duration.hide();
f.separator.hide();
Duration {
duration: f.duration,
separator: f.separator,
state: Hidden {},
}
}
}
#[derive(Debug, Clone)]
pub enum DurationMachine {
Hidden(Duration<Hidden>),
Shown(Duration<Shown>),
}
impl DurationMachine {
#[inline]
pub fn new(duration: gtk::Label, separator: gtk::Label, seconds: Option<i32>) -> Self {
let m = DurationMachine::Hidden(Duration::<Hidden>::new(duration, separator));
m.determine_state(seconds)
}
#[inline]
pub fn determine_state(self, seconds: Option<i32>) -> Self {
match (self, seconds) {
(d @ DurationMachine::Hidden(_), None) => d,
(DurationMachine::Shown(val), None) => DurationMachine::Hidden(val.into()),
(DurationMachine::Hidden(val), Some(s)) => {
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
if minutes == 0 {
DurationMachine::Hidden(val)
} else {
val.set_duration(minutes);
DurationMachine::Shown(val.into())
}
}
(DurationMachine::Shown(val), Some(s)) => {
let minutes = chrono::Duration::seconds(s.into()).num_minutes();
if minutes == 0 {
DurationMachine::Hidden(val.into())
} else {
val.set_duration(minutes);
DurationMachine::Shown(val)
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct Size<S> {
size: gtk::Label,
separator: gtk::Label,
state: S,
}
impl<S> Size<S> {
#[inline]
fn set_size(self, s: &str) -> Size<Shown> {
self.size.set_text(s);
self.size.show();
self.separator.show();
Size {
size: self.size,
separator: self.separator,
state: Shown {},
}
}
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
fn into_shown(self) -> Size<Shown> {
self.size.show();
self.separator.show();
Size {
size: self.size,
separator: self.separator,
state: Shown {},
}
}
#[inline]
fn into_hidden(self) -> Size<Hidden> {
self.size.hide();
self.separator.hide();
Size {
size: self.size,
separator: self.separator,
state: Hidden {},
}
}
}
impl Size<UnInitialized> {
#[inline]
fn new(size: gtk::Label, separator: gtk::Label) -> Self {
size.hide();
separator.hide();
Size {
size,
separator,
state: UnInitialized {},
}
}
}
// pub trait Playable {}
// impl Playable for Download {}
// impl Playable for Play {}
#[derive(Debug, Clone)]
pub struct Download;
#[derive(Debug, Clone)]
pub struct Play;
#[derive(Debug, Clone)]
// FIXME: Needs better name.
// Should each button also has it's own type and machine?
pub struct DownloadPlay<S> {
play: gtk::Button,
download: gtk::Button,
state: S,
}
impl<S> DownloadPlay<S> {
#[inline]
// https://play.rust-lang.org/?gist=1acffaf62743eeb85be1ae6ecf474784&version=stable
// It might be possible to make a generic definition with Specialization.
// https://github.com/rust-lang/rust/issues/31844
fn into_playable(self) -> DownloadPlay<Play> {
self.play.show();
self.download.hide();
DownloadPlay {
play: self.play,
download: self.download,
state: Play {},
}
}
#[inline]
fn into_fetchable(self) -> DownloadPlay<Download> {
self.play.hide();
self.download.show();
DownloadPlay {
play: self.play,
download: self.download,
state: Download {},
}
}
#[inline]
fn into_hidden(self) -> DownloadPlay<Hidden> {
self.play.hide();
self.download.hide();
DownloadPlay {
play: self.play,
download: self.download,
state: Hidden {},
}
}
#[inline]
fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
self.download.connect_clicked(f)
}
#[inline]
fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.play.connect_clicked(f)
}
}
impl DownloadPlay<UnInitialized> {
#[inline]
fn new(play: gtk::Button, download: gtk::Button) -> Self {
play.hide();
download.hide();
DownloadPlay {
play,
download,
state: UnInitialized {},
}
}
}
#[derive(Debug, Clone)]
pub struct Progress<S> {
bar: gtk::ProgressBar,
cancel: gtk::Button,
local_size: gtk::Label,
prog_separator: gtk::Label,
state: S,
}
impl<S> Progress<S> {
#[inline]
fn into_shown(self) -> Progress<Shown> {
self.bar.show();
self.cancel.show();
self.local_size.show();
self.prog_separator.show();
Progress {
bar: self.bar,
cancel: self.cancel,
local_size: self.local_size,
prog_separator: self.prog_separator,
state: Shown {},
}
}
#[inline]
fn into_hidden(self) -> Progress<Hidden> {
self.bar.hide();
self.cancel.hide();
self.local_size.hide();
self.prog_separator.hide();
Progress {
bar: self.bar,
cancel: self.cancel,
local_size: self.local_size,
prog_separator: self.prog_separator,
state: Hidden {},
}
}
#[allow(unused_must_use)]
#[inline]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn update_progress(&mut self, local_size: &str, fraction: f64) {
self.local_size.set_text(local_size);
self.bar.set_fraction(fraction);
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.cancel.connect_clicked(f)
}
}
impl Progress<UnInitialized> {
#[inline]
fn new(
bar: gtk::ProgressBar,
cancel: gtk::Button,
local_size: gtk::Label,
prog_separator: gtk::Label,
) -> Self {
bar.hide();
cancel.hide();
local_size.hide();
prog_separator.hide();
Progress {
bar,
cancel,
local_size,
prog_separator,
state: UnInitialized {},
}
}
}
#[derive(Debug, Clone)]
pub struct Media<X, Y, Z> {
dl: DownloadPlay<X>,
size: Size<Y>,
progress: Progress<Z>,
}
type New<Y> = Media<Download, Y, Hidden>;
type Playable<Y> = Media<Play, Y, Hidden>;
type InProgress = Media<Hidden, Shown, Shown>;
impl<X, Y, Z> Media<X, Y, Z> {
#[inline]
fn set_size(self, s: &str) -> Media<X, Shown, Z> {
Media {
dl: self.dl,
size: self.size.set_size(s),
progress: self.progress,
}
}
#[inline]
fn hide_size(self) -> Media<X, Hidden, Z> {
Media {
dl: self.dl,
size: self.size.into_hidden(),
progress: self.progress,
}
}
#[inline]
fn into_new(self, size: &str) -> New<Shown> {
Media {
dl: self.dl.into_fetchable(),
size: self.size.set_size(size),
progress: self.progress.into_hidden(),
}
}
#[inline]
fn into_new_without(self) -> New<Hidden> {
Media {
dl: self.dl.into_fetchable(),
size: self.size.into_hidden(),
progress: self.progress.into_hidden(),
}
}
#[inline]
fn into_playable(self, size: &str) -> Playable<Shown> {
Media {
dl: self.dl.into_playable(),
size: self.size.set_size(size),
progress: self.progress.into_hidden(),
}
}
#[inline]
fn into_playable_without(self) -> Playable<Hidden> {
Media {
dl: self.dl.into_playable(),
size: self.size.into_hidden(),
progress: self.progress.into_hidden(),
}
}
}
impl<X, Z> Media<X, Shown, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
size: self.size.into_shown(),
progress: self.progress.into_shown(),
}
}
}
impl<X, Z> Media<X, Hidden, Z> {
#[inline]
fn into_progress(self) -> InProgress {
Media {
dl: self.dl.into_hidden(),
size: self.size.set_size("Unkown"),
progress: self.progress.into_shown(),
}
}
}
impl<X, Z> Media<X, UnInitialized, Z> {
#[inline]
fn into_progress(self, size: Option<String>) -> InProgress {
if let Some(s) = size {
Media {
dl: self.dl.into_hidden(),
size: self.size.set_size(&s),
progress: self.progress.into_shown(),
}
} else {
Media {
dl: self.dl.into_hidden(),
size: self.size.set_size("Unkown"),
progress: self.progress.into_shown(),
}
}
}
}
impl InProgress {
#[inline]
#[allow(unused_must_use)]
// This does not need to be &mut since gtk-rs does not model ownership
// But I think it wouldn't hurt if we treat it as a Rust api.
fn update_progress(&mut self, local_size: &str, fraction: f64) {
self.progress.update_progress(local_size, fraction)
}
}
#[derive(Debug, Clone)]
pub enum ButtonsState {
New(Media<Download, Shown, Hidden>),
NewWithoutSize(Media<Download, Hidden, Hidden>),
Playable(Media<Play, Shown, Hidden>),
PlayableWithoutSize(Media<Play, Hidden, Hidden>),
}
impl ButtonsState {
#[inline]
pub fn determine_state(self, size: Option<String>, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
match (self, size, is_downloaded) {
// From whatever to New
(New(m), Some(s), false) => New(m.into_new(&s)),
(Playable(m), Some(s), false) => New(m.into_new(&s)),
(NewWithoutSize(m), Some(s), false) => New(m.into_new(&s)),
(PlayableWithoutSize(m), Some(s), false) => New(m.into_new(&s)),
// From whatever to Playable
(New(m), Some(s), true) => Playable(m.into_playable(&s)),
(Playable(m), Some(s), true) => Playable(m.into_playable(&s)),
(NewWithoutSize(m), Some(s), true) => Playable(m.into_playable(&s)),
(PlayableWithoutSize(m), Some(s), true) => Playable(m.into_playable(&s)),
// From whatever to NewWithoutSize
(New(m), None, false) => NewWithoutSize(m.hide_size()),
(Playable(m), None, false) => NewWithoutSize(m.into_new_without()),
(b @ NewWithoutSize(_), None, false) => b,
(PlayableWithoutSize(m), None, false) => NewWithoutSize(m.into_new_without()),
// From whatever to PlayableWithoutSize
(New(m), None, true) => PlayableWithoutSize(m.into_playable_without()),
(Playable(m), None, true) => PlayableWithoutSize(m.hide_size()),
(NewWithoutSize(val), None, true) => PlayableWithoutSize(val.into_playable_without()),
(b @ PlayableWithoutSize(_), None, true) => b,
}
}
#[inline]
fn into_progress(self) -> InProgress {
use self::ButtonsState::*;
match self {
New(m) => m.into_progress(),
Playable(m) => m.into_progress(),
NewWithoutSize(m) => m.into_progress(),
PlayableWithoutSize(m) => m.into_progress(),
}
}
#[inline]
fn set_size(self, size: Option<String>) -> Self {
use self::ButtonsState::*;
match (self, size) {
(New(m), Some(s)) => New(m.set_size(&s)),
(New(m), None) => NewWithoutSize(m.hide_size()),
(Playable(m), Some(s)) => Playable(m.set_size(&s)),
(Playable(m), None) => PlayableWithoutSize(m.hide_size()),
(bttn @ NewWithoutSize(_), None) => bttn,
(bttn @ PlayableWithoutSize(_), None) => bttn,
(NewWithoutSize(m), Some(s)) => New(m.into_new(&s)),
(PlayableWithoutSize(m), Some(s)) => Playable(m.into_playable(&s)),
}
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.dl.download_connect_clicked(f),
NewWithoutSize(ref val) => val.dl.download_connect_clicked(f),
Playable(ref val) => val.dl.download_connect_clicked(f),
PlayableWithoutSize(ref val) => val.dl.download_connect_clicked(f),
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.dl.play_connect_clicked(f),
NewWithoutSize(ref val) => val.dl.play_connect_clicked(f),
Playable(ref val) => val.dl.play_connect_clicked(f),
PlayableWithoutSize(ref val) => val.dl.play_connect_clicked(f),
}
}
#[inline]
fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(&self, f: F) -> glib::SignalHandlerId {
use self::ButtonsState::*;
match *self {
New(ref val) => val.progress.cancel_connect_clicked(f),
NewWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
Playable(ref val) => val.progress.cancel_connect_clicked(f),
PlayableWithoutSize(ref val) => val.progress.cancel_connect_clicked(f),
}
}
}
#[derive(Debug, Clone)]
pub enum MediaMachine {
UnInitialized(Media<UnInitialized, UnInitialized, UnInitialized>),
Initialized(ButtonsState),
InProgress(Media<Hidden, Shown, Shown>),
}
impl MediaMachine {
#[cfg_attr(feature = "cargo-clippy", allow(too_many_arguments))]
#[inline]
pub fn new(
play: gtk::Button,
download: gtk::Button,
bar: gtk::ProgressBar,
cancel: gtk::Button,
total_size: gtk::Label,
local_size: gtk::Label,
separator: gtk::Label,
prog_separator: gtk::Label,
) -> Self {
let dl = DownloadPlay::<UnInitialized>::new(play, download);
let progress = Progress::<UnInitialized>::new(bar, cancel, local_size, prog_separator);
let size = Size::<UnInitialized>::new(total_size, separator);
MediaMachine::UnInitialized(Media { dl, progress, size })
}
#[inline]
pub fn download_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.dl.download_connect_clicked(f),
Initialized(ref val) => val.download_connect_clicked(f),
InProgress(ref val) => val.dl.download_connect_clicked(f),
}
}
#[inline]
pub fn play_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.dl.play_connect_clicked(f),
Initialized(ref val) => val.play_connect_clicked(f),
InProgress(ref val) => val.dl.play_connect_clicked(f),
}
}
#[inline]
pub fn cancel_connect_clicked<F: Fn(&gtk::Button) + 'static>(
&self,
f: F,
) -> glib::SignalHandlerId {
use self::MediaMachine::*;
match *self {
UnInitialized(ref val) => val.progress.cancel_connect_clicked(f),
Initialized(ref val) => val.cancel_connect_clicked(f),
InProgress(ref val) => val.progress.cancel_connect_clicked(f),
}
}
#[inline]
pub fn determine_state(self, bytes: Option<i32>, is_active: bool, is_downloaded: bool) -> Self {
use self::ButtonsState::*;
use self::MediaMachine::*;
match (self, size_helper(bytes), is_downloaded, is_active) {
(UnInitialized(m), s, _, true) => InProgress(m.into_progress(s)),
// Into New
(UnInitialized(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(UnInitialized(m), None, false, false) => {
Initialized(NewWithoutSize(m.into_new_without()))
}
// Into Playable
(UnInitialized(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(UnInitialized(m), None, true, false) => {
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(Initialized(bttn), s, dl, false) => Initialized(bttn.determine_state(s, dl)),
(Initialized(bttn), _, _, true) => InProgress(bttn.into_progress()),
// Into New
(InProgress(m), Some(s), false, false) => Initialized(New(m.into_new(&s))),
(InProgress(m), None, false, false) => {
Initialized(NewWithoutSize(m.into_new_without()))
}
// Into Playable
(InProgress(m), Some(s), true, false) => Initialized(Playable(m.into_playable(&s))),
(InProgress(m), None, true, false) => {
Initialized(PlayableWithoutSize(m.into_playable_without()))
}
(i @ InProgress(_), _, _, _) => i,
}
}
#[inline]
pub fn set_size(self, bytes: Option<i32>) -> Self {
use self::MediaMachine::*;
let size = size_helper(bytes);
match (self, size) {
(Initialized(bttn), s) => Initialized(bttn.set_size(s)),
(InProgress(val), Some(s)) => InProgress(val.set_size(&s)),
(n @ InProgress(_), None) => n,
(n @ UnInitialized(_), _) => n,
}
}
#[inline]
pub fn update_progress(&mut self, local_size: &str, fraction: f64) {
use self::MediaMachine::*;
match *self {
Initialized(_) => (),
UnInitialized(_) => (),
InProgress(ref mut val) => val.update_progress(local_size, fraction),
}
}
}
#[inline]
fn size_helper(bytes: Option<i32>) -> Option<String> {
let s = bytes?;
if s == 0 {
return None;
}
s.file_size(SIZE_OPTS.clone()).ok()
}
-361
View File
@@ -1,361 +0,0 @@
use glib;
use gtk;
use gtk::prelude::*;
use failure::Error;
use html2pango::markup_from_raw;
use open;
use rayon;
use send_cell::SendCell;
use hammond_data::dbqueries;
use hammond_data::utils::delete_show;
use hammond_data::Podcast;
use app::Action;
use appnotif::InAppNotification;
use utils::{self, lazy_load};
use widgets::EpisodeWidget;
use std::rc::Rc;
use std::sync::mpsc::{SendError, Sender};
use std::sync::{Arc, Mutex};
lazy_static! {
static ref SHOW_WIDGET_VALIGNMENT: Mutex<Option<(i32, SendCell<gtk::Adjustment>)>> =
Mutex::new(None);
}
#[derive(Debug, Clone)]
pub struct ShowWidget {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
cover: gtk::Image,
description: gtk::Label,
link: gtk::Button,
settings: gtk::MenuButton,
unsub: gtk::Button,
episodes: gtk::ListBox,
podcast_id: Option<i32>,
}
impl Default for ShowWidget {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
let container: gtk::Box = builder.get_object("container").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let episodes = builder.get_object("episodes").unwrap();
let cover: gtk::Image = builder.get_object("cover").unwrap();
let description: gtk::Label = builder.get_object("description").unwrap();
let unsub: gtk::Button = builder.get_object("unsub_button").unwrap();
let link: gtk::Button = builder.get_object("link_button").unwrap();
let settings: gtk::MenuButton = builder.get_object("settings_button").unwrap();
ShowWidget {
container,
scrolled_window,
cover,
description,
unsub,
link,
settings,
episodes,
podcast_id: None,
}
}
}
impl ShowWidget {
#[inline]
pub fn new(pd: Arc<Podcast>, sender: Sender<Action>) -> Rc<ShowWidget> {
let mut pdw = ShowWidget::default();
pdw.init(&pd, &sender);
let pdw = Rc::new(pdw);
populate_listbox(&pdw, pd, sender)
.map_err(|err| error!("Failed to populate the listbox: {}", err))
.ok();
pdw
}
#[inline]
pub fn init(&mut self, pd: &Arc<Podcast>, sender: &Sender<Action>) {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/show_widget.ui");
self.unsub
.connect_clicked(clone!(pd, sender => move |bttn| {
on_unsub_button_clicked(pd.clone(), bttn, &sender);
}));
self.set_description(pd.description());
self.podcast_id = Some(pd.id());
self.set_cover(&pd)
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
let link = pd.link().to_owned();
self.link.set_tooltip_text(Some(link.as_str()));
self.link.connect_clicked(move |_| {
info!("Opening link: {}", &link);
open::that(&link)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed open link: {}", &link))
.ok();
});
let show_menu: gtk::Popover = builder.get_object("show_menu").unwrap();
let mark_all: gtk::ModelButton = builder.get_object("mark_all_watched").unwrap();
let episodes = self.episodes.clone();
mark_all.connect_clicked(clone!(pd, sender => move |_| {
on_played_button_clicked(
pd.clone(),
&episodes,
&sender
)
}));
self.settings.set_popover(&show_menu);
}
#[inline]
/// Set the show cover.
fn set_cover(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
utils::set_image_from_path(&self.cover, pd.id(), 256)
}
#[inline]
/// Set the descripton text.
fn set_description(&self, text: &str) {
self.description.set_markup(&markup_from_raw(text));
}
#[inline]
/// Save the scrollabar vajustment to the cache.
pub fn save_vadjustment(&self, oldid: i32) -> Result<(), Error> {
if let Ok(mut guard) = SHOW_WIDGET_VALIGNMENT.lock() {
let adj = self.scrolled_window
.get_vadjustment()
.ok_or_else(|| format_err!("Could not get the adjustment"))?;
*guard = Some((oldid, SendCell::new(adj)));
debug!("Widget Alignment was saved with ID: {}.", oldid);
}
Ok(())
}
#[inline]
/// Set scrolled window vertical adjustment.
fn set_vadjustment(&self, pd: &Arc<Podcast>) -> Result<(), Error> {
let guard = SHOW_WIDGET_VALIGNMENT
.lock()
.map_err(|err| format_err!("Failed to lock widget align mutex: {}", err))?;
if let Some((oldid, ref sendcell)) = *guard {
// Only copy the old scrollbar if both widget's represent the same podcast.
debug!("PID: {}", pd.id());
debug!("OLDID: {}", oldid);
if pd.id() != oldid {
debug!("Early return");
return Ok(());
};
// Copy the vertical scrollbar adjustment from the old view into the new one.
sendcell
.try_get()
.map(|x| utils::smooth_scroll_to(&self.scrolled_window, &x));
}
Ok(())
}
pub fn podcast_id(&self) -> Option<i32> {
self.podcast_id
}
}
#[inline]
/// Populate the listbox with the shows episodes.
fn populate_listbox(
show: &Rc<ShowWidget>,
pd: Arc<Podcast>,
sender: Sender<Action>,
) -> Result<(), Error> {
use crossbeam_channel::bounded;
use crossbeam_channel::TryRecvError::*;
let count = dbqueries::get_pd_episodes_count(&pd)?;
let (sender_, receiver) = bounded(1);
rayon::spawn(clone!(pd => move || {
let episodes = dbqueries::get_pd_episodeswidgets(&pd).unwrap();
// The receiver can be dropped if there's an early return
// like on show without episodes for example.
sender_.send(episodes).ok();
}));
if count == 0 {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/empty_show.ui");
let container: gtk::Box = builder.get_object("empty_show").unwrap();
show.episodes.add(&container);
return Ok(());
}
let show_ = show.clone();
gtk::idle_add(move || {
let episodes = match receiver.try_recv() {
Ok(e) => e,
Err(Empty) => return glib::Continue(true),
Err(Disconnected) => return glib::Continue(false),
};
let list = show_.episodes.clone();
let constructor = clone!(sender => move |ep| {
EpisodeWidget::new(ep, &sender).container
});
let callback = clone!(pd, show_ => move || {
show_.set_vadjustment(&pd)
.map_err(|err| error!("Failed to set ShowWidget Alignment: {}", err))
.ok();
});
lazy_load(episodes, list.clone(), constructor, callback);
glib::Continue(false)
});
Ok(())
}
#[inline]
fn on_unsub_button_clicked(pd: Arc<Podcast>, unsub_button: &gtk::Button, sender: &Sender<Action>) {
// hack to get away without properly checking for none.
// if pressed twice would panic.
unsub_button.set_sensitive(false);
let wrap = || -> Result<(), SendError<_>> {
sender.send(Action::RemoveShow(pd))?;
sender.send(Action::HeaderBarNormal)?;
sender.send(Action::ShowShowsAnimated)?;
// Queue a refresh after the switch to avoid blocking the db.
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
wrap().map_err(|err| error!("Action Sender: {}", err)).ok();
unsub_button.set_sensitive(true);
}
#[inline]
fn on_played_button_clicked(pd: Arc<Podcast>, episodes: &gtk::ListBox, sender: &Sender<Action>) {
if dim_titles(episodes).is_none() {
error!("Something went horribly wrong when dimming the titles.");
warn!("RUN WHILE YOU STILL CAN!");
}
sender
.send(Action::MarkAllPlayerNotification(pd))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
}
#[inline]
fn mark_all_watched(pd: &Podcast, sender: &Sender<Action>) -> Result<(), Error> {
dbqueries::update_none_to_played_now(pd)?;
// Not all widgets migth have been loaded when the mark_all is hit
// So we will need to refresh again after it's done.
sender.send(Action::RefreshWidgetIfSame(pd.id()))?;
sender.send(Action::RefreshEpisodesView).map_err(From::from)
}
#[inline]
pub fn mark_all_notif(pd: Arc<Podcast>, sender: &Sender<Action>) -> InAppNotification {
let id = pd.id();
let callback = clone!(sender => move || {
mark_all_watched(&pd, &sender)
.map_err(|err| error!("Notif Callback Error: {}", err))
.ok();
glib::Continue(false)
});
let undo_callback = clone!(sender => move || {
sender.send(Action::RefreshWidgetIfSame(id))
.map_err(|err| error!("Action Sender: {}", err))
.ok();
});
let text = "Marked all episodes as listened";
InAppNotification::new(text, callback, undo_callback)
}
#[inline]
pub fn remove_show_notif(pd: Arc<Podcast>, sender: Sender<Action>) -> InAppNotification {
let text = format!("Unsubscribed from {}", pd.title());
utils::ignore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not insert {} to the ignore list.", pd.title()))
.ok();
let callback = clone!(pd, sender => move || {
utils::uningore_show(pd.id())
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Could not remove {} from the ignore list.", pd.title()))
.ok();
// Spawn a thread so it won't block the ui.
rayon::spawn(clone!(pd, sender => move || {
delete_show(&pd)
.map_err(|err| error!("Error: {}", err))
.map_err(|_| error!("Failed to delete {}", pd.title()))
.ok();
sender.send(Action::RefreshEpisodesView).ok();
}));
glib::Continue(false)
});
let undo_wrap = move || -> Result<(), Error> {
utils::uningore_show(pd.id())?;
sender.send(Action::RefreshShowsView)?;
sender.send(Action::RefreshEpisodesView)?;
Ok(())
};
let undo_callback = move || {
undo_wrap().map_err(|err| error!("{}", err)).ok();
};
InAppNotification::new(&text, callback, undo_callback)
}
#[inline]
// Ideally if we had a custom widget this would have been as simple as:
// `for row in listbox { ep = row.get_episode(); ep.dim_title(); }`
// But now I can't think of a better way to do it than hardcoding the title
// position relative to the EpisodeWidget container gtk::Box.
fn dim_titles(episodes: &gtk::ListBox) -> Option<()> {
let children = episodes.get_children();
for row in children {
let row = row.downcast::<gtk::ListBoxRow>().ok()?;
let container = row.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let foo = container
.get_children()
.remove(0)
.downcast::<gtk::Box>()
.ok()?;
let bar = foo.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let baz = bar.get_children().remove(0).downcast::<gtk::Box>().ok()?;
let title = baz.get_children().remove(0).downcast::<gtk::Label>().ok()?;
title.get_style_context().map(|c| c.add_class("dim-label"));
}
Some(())
}
-137
View File
@@ -1,137 +0,0 @@
use failure::Error;
use gtk;
use gtk::prelude::*;
use hammond_data::dbqueries;
use hammond_data::Podcast;
use app::Action;
use utils::{get_ignored_shows, lazy_load, set_image_from_path};
use std::rc::Rc;
use std::sync::mpsc::Sender;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ShowsView {
pub container: gtk::Box,
scrolled_window: gtk::ScrolledWindow,
flowbox: gtk::FlowBox,
}
impl Default for ShowsView {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_view.ui");
let container: gtk::Box = builder.get_object("fb_parent").unwrap();
let scrolled_window: gtk::ScrolledWindow = builder.get_object("scrolled_window").unwrap();
let flowbox: gtk::FlowBox = builder.get_object("flowbox").unwrap();
ShowsView {
container,
scrolled_window,
flowbox,
}
}
}
impl ShowsView {
#[inline]
pub fn new(sender: Sender<Action>) -> Result<Rc<Self>, Error> {
let pop = Rc::new(ShowsView::default());
pop.init(sender)?;
Ok(pop)
}
#[inline]
pub fn init(&self, sender: Sender<Action>) -> Result<(), Error> {
self.flowbox.connect_child_activated(move |_, child| {
on_child_activate(child, &sender)
.map_err(|err| error!("Error along flowbox child activation: {}", err))
.ok();
});
// Populate the flowbox with the Podcasts.
self.populate_flowbox()
}
#[inline]
fn populate_flowbox(&self) -> Result<(), Error> {
let ignore = get_ignored_shows()?;
let podcasts = dbqueries::get_podcasts_filter(&ignore)?;
let flowbox = self.flowbox.clone();
let constructor = |parent| ShowsChild::new(&parent).child;
let callback = move || flowbox.show_all();
let flowbox = self.flowbox.clone();
lazy_load(podcasts, flowbox, constructor, callback);
Ok(())
}
}
#[inline]
fn on_child_activate(child: &gtk::FlowBoxChild, sender: &Sender<Action>) -> Result<(), Error> {
use gtk::WidgetExt;
// This is such an ugly hack...
let id = WidgetExt::get_name(child)
.ok_or_else(|| format_err!("Faild to get \"episodes\" child from the stack."))?
.parse::<i32>()?;
let pd = Arc::new(dbqueries::get_podcast_from_id(id)?);
sender.send(Action::HeaderBarShowTile(pd.title().into()))?;
sender.send(Action::ReplaceWidget(pd))?;
sender.send(Action::ShowWidgetAnimated)?;
Ok(())
}
#[derive(Debug)]
struct ShowsChild {
container: gtk::Box,
cover: gtk::Image,
child: gtk::FlowBoxChild,
}
impl Default for ShowsChild {
#[inline]
fn default() -> Self {
let builder = gtk::Builder::new_from_resource("/org/gnome/hammond/gtk/shows_child.ui");
let container: gtk::Box = builder.get_object("fb_child").unwrap();
let cover: gtk::Image = builder.get_object("pd_cover").unwrap();
let child = gtk::FlowBoxChild::new();
child.add(&container);
ShowsChild {
container,
cover,
child,
}
}
}
impl ShowsChild {
#[inline]
pub fn new(pd: &Podcast) -> ShowsChild {
let child = ShowsChild::default();
child.init(pd);
child
}
#[inline]
fn init(&self, pd: &Podcast) {
self.container.set_tooltip_text(pd.title());
WidgetExt::set_name(&self.child, &pd.id().to_string());
self.set_cover(pd.id())
.map_err(|err| error!("Failed to set a cover: {}", err))
.ok();
}
#[inline]
fn set_cover(&self, podcast_id: i32) -> Result<(), Error> {
set_image_from_path(&self.cover, podcast_id, 256)
}
}
+16 -13
View File
@@ -2,23 +2,26 @@
# https://gitlab.gnome.org/danigm/fractal/blob/6e2911f9d2353c99a18a6c19fab7f903c4bbb431/meson.build
project(
'hammond', 'rust',
version: '0.3.2',
'gnome-podcasts', 'rust',
version: '0.4.3',
license: 'GPLv3',
)
hammond_version = meson.project_version()
version_array = hammond_version.split('.')
hammond_major_version = version_array[0].to_int()
hammond_minor_version = version_array[1].to_int()
hammond_version_micro = version_array[2].to_int()
podcasts_version = meson.project_version()
version_array = podcasts_version.split('.')
podcasts_major_version = version_array[0].to_int()
podcasts_minor_version = version_array[1].to_int()
podcasts_version_micro = version_array[2].to_int()
hammond_prefix = get_option('prefix')
hammond_bindir = join_paths(hammond_prefix, get_option('bindir'))
podcasts_prefix = get_option('prefix')
podcasts_bindir = join_paths(podcasts_prefix, get_option('bindir'))
podcasts_conf = configuration_data()
podcasts_conf.set('BINDIR', podcasts_bindir)
datadir = get_option('datadir')
icondir = join_paths(datadir, 'icons')
subdir('hammond-gtk/resources')
subdir('podcasts-gtk/resources')
cargo = find_program('cargo', required: false)
gresource = find_program('glib-compile-resources', required: false)
@@ -28,11 +31,11 @@ cargo_script = find_program('scripts/cargo.sh')
cargo_release = custom_target('cargo-build',
build_by_default: true,
build_always: true,
output: ['hammond'],
output: ['gnome-podcasts'],
install: true,
install_dir: hammond_bindir,
install_dir: podcasts_bindir,
command: [cargo_script, '@CURRENT_SOURCE_DIR@', '@OUTPUT@'])
run_target('release', command: ['scripts/release.sh',
meson.project_name() + '-' + hammond_version
meson.project_name() + '-' + podcasts_version
])
@@ -1,12 +1,12 @@
{
"app-id" : "org.gnome.Hammond",
"app-id" : "org.gnome.Podcasts",
"runtime" : "org.gnome.Platform",
"runtime-version" : "master",
"runtime-version" : "3.28",
"sdk" : "org.gnome.Sdk",
"sdk-extensions" : [
"org.freedesktop.Sdk.Extension.rust-stable"
],
"command" : "hammond",
"command" : "gnome-podcasts",
"tags" : [
"nightly"
],
@@ -20,6 +20,7 @@
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--talk-name=org.freedesktop.Desktop"
],
"build-options" : {
@@ -28,19 +29,19 @@
"--share=network"
],
"env" : {
"CARGO_HOME" : "/run/build/Hammond/cargo",
"CARGO_HOME" : "/run/build/Podcasts/cargo",
"RUST_BACKTRACE" : "1",
"RUSTFLAGS" : "--cfg rayon_unstable"
"DEBUG" : "true"
}
},
"modules" : [
{
"name" : "hammond",
"name" : "gnome-podcasts",
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/World/hammond.git",
"url" : "https://gitlab.gnome.org/World/podcasts.git",
"branch" : "master"
}
]
@@ -1,23 +1,24 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-data"
name = "podcasts-data"
version = "0.1.0"
workspace = "../"
[dependencies]
ammonia = "1.1.0"
chrono = "0.4.2"
ammonia = "1.2.0"
chrono = "0.4.4"
derive_builder = "0.5.1"
lazy_static = "1.0.0"
log = "0.4.1"
rayon = "1.0.1"
lazy_static = "1.0.2"
log = "0.4.3"
rayon = "1.0.2"
rayon-futures = "0.1.0"
rfc822_sanitizer = "0.3.3"
rss = "1.5.0"
url = "1.7.0"
url = "1.7.1"
xdg = "2.1.0"
futures = "0.1.21"
hyper = "0.11.25"
xml-rs = "0.8.0"
futures = "0.1.23"
hyper = "0.11.27"
tokio-core = "0.1.17"
hyper-tls = "0.1.3"
native-tls = "0.1.5"
@@ -25,24 +26,20 @@ num_cpus = "1.8.0"
failure = "0.1.1"
failure_derive = "0.1.1"
# newer stuff do not compile inside flatpak
html5ever = "= 0.22.0"
string_cache = "= 0.7.1"
string_cache_codegen = "= 0.4.0"
[dependencies.diesel]
features = ["sqlite", "r2d2"]
version = "1.2.2"
version = "1.3.2"
[dependencies.diesel_migrations]
features = ["sqlite"]
version = "1.2.0"
version = "1.3.0"
[dev-dependencies]
rand = "0.4.2"
rand = "0.5.4"
tempdir = "0.3.7"
criterion = "0.2.3"
criterion = "0.2.4"
pretty_assertions = "0.5.1"
maplit = "1.0.1"
[[bench]]
name = "bench"
@@ -5,9 +5,9 @@ extern crate criterion;
use criterion::Criterion;
// extern crate futures;
extern crate hammond_data;
extern crate hyper;
extern crate hyper_tls;
extern crate podcasts_data;
extern crate rand;
extern crate tokio_core;
// extern crate rayon;
@@ -18,11 +18,11 @@ extern crate rss;
// use futures::future::*;
use tokio_core::reactor::Core;
use hammond_data::database::truncate_db;
use hammond_data::pipeline;
use hammond_data::FeedBuilder;
use hammond_data::Source;
// use hammond_data::errors::*;
use podcasts_data::database::truncate_db;
use podcasts_data::pipeline;
use podcasts_data::FeedBuilder;
use podcasts_data::Source;
// use podcasts_data::errors::*;
use std::io::BufReader;
+6
View File
@@ -0,0 +1,6 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
patch_file = "src/schema.patch"
@@ -0,0 +1,53 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
favorite INTEGER DEFAULT 0,
archive INTEGER DEFAULT 0,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, favorite, archive)
SELECT title, uri, local_uri, description, epoch, length, duration, guid, played, podcast_id, 0, 0
FROM old_table;
Drop table old_table;
ALTER TABLE podcast RENAME TO old_table;
CREATE TABLE `podcast` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`source_id` INTEGER NOT NULL UNIQUE,
`favorite` INTEGER NOT NULL DEFAULT 0,
`archive` INTEGER NOT NULL DEFAULT 0,
`always_dl` INTEGER NOT NULL DEFAULT 0
);
INSERT INTO podcast (
id,
title,
link,
description,
image_uri,
source_id
) SELECT id,
title,
link,
description,
image_uri,
source_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,66 @@
ALTER TABLE episode RENAME TO old_table;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
podcast_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
podcast_id
FROM old_table;
Drop table old_table;
ALTER TABLE podcast RENAME TO old_table;
CREATE TABLE `podcast` (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
`title` TEXT NOT NULL,
`link` TEXT NOT NULL,
`description` TEXT NOT NULL,
`image_uri` TEXT,
`source_id` INTEGER NOT NULL UNIQUE
);
INSERT INTO podcast (
id,
title,
link,
description,
image_uri,
source_id
) SELECT id,
title,
link,
description,
image_uri,
source_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,40 @@
ALTER TABLE episodes RENAME TO old_table;
ALTER TABLE shows RENAME TO podcast;
CREATE TABLE episode (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
podcast_id INTEGER NOT NULL,
PRIMARY KEY (title, podcast_id)
);
INSERT INTO episode (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
podcast_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
show_id
FROM old_table;
Drop table old_table;
@@ -0,0 +1,40 @@
ALTER TABLE episode RENAME TO old_table;
ALTER TABLE podcast RENAME TO shows;
CREATE TABLE episodes (
title TEXT NOT NULL,
uri TEXT,
local_uri TEXT,
description TEXT,
epoch INTEGER NOT NULL DEFAULT 0,
length INTEGER,
duration INTEGER,
guid TEXT,
played INTEGER,
show_id INTEGER NOT NULL,
PRIMARY KEY (title, show_id)
);
INSERT INTO episodes (
title,
uri,
local_uri,
description,
epoch,
length,
duration,
guid,
played,
show_id
) SELECT title,
uri,
local_uri,
description,
epoch, length,
duration,
guid,
played,
podcast_id
FROM old_table;
Drop table old_table;
@@ -1,4 +1,6 @@
//! Database Setup. This is only public to help with some unit tests.
// Diesel embed_migrations! triggers the lint
#![allow(unused_imports)]
use diesel::prelude::*;
use diesel::r2d2;
@@ -22,7 +24,9 @@ lazy_static! {
#[cfg(not(test))]
lazy_static! {
static ref DB_PATH: PathBuf = xdg_dirs::HAMMOND_XDG.place_data_file("hammond.db").unwrap();
static ref DB_PATH: PathBuf = xdg_dirs::PODCASTS_XDG
.place_data_file("podcasts.db")
.unwrap();
}
#[cfg(test)]
@@ -30,8 +34,8 @@ extern crate tempdir;
#[cfg(test)]
lazy_static! {
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("hammond_unit_test").unwrap() };
static ref DB_PATH: PathBuf = TEMPDIR.path().join("hammond.db");
static ref TEMPDIR: tempdir::TempDir = { tempdir::TempDir::new("podcasts_unit_test").unwrap() };
static ref DB_PATH: PathBuf = TEMPDIR.path().join("podcasts.db");
}
/// Get an r2d2 `SqliteConnection`.
@@ -66,8 +70,8 @@ fn run_migration_on(connection: &SqliteConnection) -> Result<(), DataError> {
pub fn truncate_db() -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
con.execute("DELETE FROM episode")?;
con.execute("DELETE FROM podcast")?;
con.execute("DELETE FROM episodes")?;
con.execute("DELETE FROM shows")?;
con.execute("DELETE FROM source")?;
Ok(())
}
@@ -23,106 +23,106 @@ pub fn get_sources() -> Result<Vec<Source>, DataError> {
.map_err(From::from)
}
pub fn get_podcasts() -> Result<Vec<Podcast>, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcasts() -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.order(title.asc())
.load::<Podcast>(&con)
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Podcast>, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcasts_filter(filter_ids: &[i32]) -> Result<Vec<Show>, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.order(title.asc())
.filter(id.ne_all(filter_ids))
.load::<Podcast>(&con)
.load::<Show>(&con)
.map_err(From::from)
}
pub fn get_episodes() -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.order(epoch.desc())
.load::<Episode>(&con)
.map_err(From::from)
}
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
use schema::episode::dsl::*;
pub(crate) fn get_downloaded_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((rowid, local_uri, played))
.filter(local_uri.is_not_null())
.load::<EpisodeCleanerQuery>(&con)
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
// pub(crate) fn get_played_episodes() -> Result<Vec<Episode>, DataError> {
// use schema::episode::dsl::*;
// use schema::episodes::dsl::*;
// let db = connection();
// let con = db.get()?;
// episode
// episodes
// .filter(played.is_not_null())
// .load::<Episode>(&con)
// .map_err(From::from)
// }
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerQuery>, DataError> {
use schema::episode::dsl::*;
pub(crate) fn get_played_cleaner_episodes() -> Result<Vec<EpisodeCleanerModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((rowid, local_uri, played))
.filter(played.is_not_null())
.load::<EpisodeCleanerQuery>(&con)
.load::<EpisodeCleanerModel>(&con)
.map_err(From::from)
}
pub fn get_episode_from_rowid(ep_id: i32) -> Result<Episode, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(rowid.eq(ep_id))
.get_result::<Episode>(&con)
.map_err(From::from)
}
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetQuery, DataError> {
use schema::episode::dsl::*;
pub fn get_episode_widget_from_rowid(ep_id: i32) -> Result<EpisodeWidgetModel, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.select((
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
))
.filter(rowid.eq(ep_id))
.get_result::<EpisodeWidgetQuery>(&con)
.get_result::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(rowid.eq(ep_id))
.select(local_uri)
.get_result::<Option<String>>(&con)
@@ -132,48 +132,48 @@ pub fn get_episode_local_uri_from_id(ep_id: i32) -> Result<Option<String>, DataE
pub fn get_episodes_widgets_filter_limit(
filter_ids: &[i32],
limit: u32,
) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
) -> Result<Vec<EpisodeWidgetModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
);
episode
episodes
.select(columns)
.order(epoch.desc())
.filter(podcast_id.ne_all(filter_ids))
.filter(show_id.ne_all(filter_ids))
.limit(i64::from(limit))
.load::<EpisodeWidgetQuery>(&con)
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_podcast_from_id(pid: i32) -> Result<Podcast, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_from_id(pid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.filter(id.eq(pid))
.get_result::<Podcast>(&con)
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_podcast_cover_from_id(pid: i32) -> Result<PodcastCoverQuery, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_cover_from_id(pid: i32) -> Result<ShowCoverModel, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.select((id, title, image_uri))
.filter(id.eq(pid))
.get_result::<PodcastCoverQuery>(&con)
.get_result::<ShowCoverModel>(&con)
.map_err(From::from)
}
pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -183,7 +183,7 @@ pub fn get_pd_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
pub fn get_pd_episodes_count(parent: &Show) -> Result<i64, DataError> {
let db = connection();
let con = db.get()?;
@@ -193,24 +193,24 @@ pub fn get_pd_episodes_count(parent: &Podcast) -> Result<i64, DataError> {
.map_err(From::from)
}
pub fn get_pd_episodeswidgets(parent: &Podcast) -> Result<Vec<EpisodeWidgetQuery>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_episodeswidgets(parent: &Show) -> Result<Vec<EpisodeWidgetModel>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
let columns = (
rowid, title, uri, local_uri, epoch, length, duration, played, podcast_id,
rowid, title, uri, local_uri, epoch, length, duration, played, show_id,
);
episode
episodes
.select(columns)
.filter(podcast_id.eq(parent.id()))
.filter(show_id.eq(parent.id()))
.order(epoch.desc())
.load::<EpisodeWidgetQuery>(&con)
.load::<EpisodeWidgetModel>(&con)
.map_err(From::from)
}
pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataError> {
use schema::episode::dsl::*;
pub fn get_pd_unplayed_episodes(parent: &Show) -> Result<Vec<Episode>, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -221,8 +221,8 @@ pub fn get_pd_unplayed_episodes(parent: &Podcast) -> Result<Vec<Episode>, DataEr
.map_err(From::from)
}
// pub(crate) fn get_pd_episodes_limit(parent: &Podcast, limit: u32) ->
// Result<Vec<Episode>, DataError> { use schema::episode::dsl::*;
// pub(crate) fn get_pd_episodes_limit(parent: &Show, limit: u32) ->
// Result<Vec<Episode>, DataError> { use schema::episodes::dsl::*;
// let db = connection();
// let con = db.get()?;
@@ -256,25 +256,25 @@ pub fn get_source_from_id(id_: i32) -> Result<Source, DataError> {
.map_err(From::from)
}
pub fn get_podcast_from_source_id(sid: i32) -> Result<Podcast, DataError> {
use schema::podcast::dsl::*;
pub fn get_podcast_from_source_id(sid: i32) -> Result<Show, DataError> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
podcast
shows
.filter(source_id.eq(sid))
.get_result::<Podcast>(&con)
.get_result::<Show>(&con)
.map_err(From::from)
}
pub fn get_episode_from_pk(title_: &str, pid: i32) -> Result<Episode, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
episodes
.filter(title.eq(title_))
.filter(podcast_id.eq(pid))
.filter(show_id.eq(pid))
.get_result::<Episode>(&con)
.map_err(From::from)
}
@@ -283,19 +283,19 @@ pub(crate) fn get_episode_minimal_from_pk(
title_: &str,
pid: i32,
) -> Result<EpisodeMinimal, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
episode
.select((rowid, title, uri, epoch, duration, guid, podcast_id))
episodes
.select((rowid, title, uri, epoch, length, duration, guid, show_id))
.filter(title.eq(title_))
.filter(podcast_id.eq(pid))
.filter(show_id.eq(pid))
.get_result::<EpisodeMinimal>(&con)
.map_err(From::from)
}
pub(crate) fn remove_feed(pd: &Podcast) -> Result<(), DataError> {
pub(crate) fn remove_feed(pd: &Show) -> Result<(), DataError> {
let db = connection();
let con = db.get()?;
@@ -314,16 +314,16 @@ fn delete_source(con: &SqliteConnection, source_id: i32) -> QueryResult<usize> {
diesel::delete(source.filter(id.eq(source_id))).execute(con)
}
fn delete_podcast(con: &SqliteConnection, podcast_id: i32) -> QueryResult<usize> {
use schema::podcast::dsl::*;
fn delete_podcast(con: &SqliteConnection, show_id: i32) -> QueryResult<usize> {
use schema::shows::dsl::*;
diesel::delete(podcast.filter(id.eq(podcast_id))).execute(con)
diesel::delete(shows.filter(id.eq(show_id))).execute(con)
}
fn delete_podcast_episodes(con: &SqliteConnection, parent_id: i32) -> QueryResult<usize> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
diesel::delete(episode.filter(podcast_id.eq(parent_id))).execute(con)
diesel::delete(episodes.filter(show_id.eq(parent_id))).execute(con)
}
pub fn source_exists(url: &str) -> Result<bool, DataError> {
@@ -338,70 +338,70 @@ pub fn source_exists(url: &str) -> Result<bool, DataError> {
}
pub(crate) fn podcast_exists(source_id_: i32) -> Result<bool, DataError> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(podcast.filter(source_id.eq(source_id_))))
select(exists(shows.filter(source_id.eq(source_id_))))
.get_result(&con)
.map_err(From::from)
}
#[cfg_attr(rustfmt, rustfmt_skip)]
pub(crate) fn episode_exists(title_: &str, podcast_id_: i32) -> Result<bool, DataError> {
use schema::episode::dsl::*;
pub(crate) fn episode_exists(title_: &str, show_id_: i32) -> Result<bool, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.filter(podcast_id.eq(podcast_id_)).filter(title.eq(title_))))
select(exists(episodes.filter(show_id.eq(show_id_)).filter(title.eq(title_))))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `episode table contains any rows
/// Check if the `episodes table contains any rows
///
/// Return true if `episode` table is populated.
/// Return true if `episodes` table is populated.
pub fn is_episodes_populated() -> Result<bool, DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(episode.as_query()))
select(exists(episodes.as_query()))
.get_result(&con)
.map_err(From::from)
}
/// Check if the `podcast` table contains any rows
/// Check if the `shows` table contains any rows
///
/// Return true if `podcast table is populated.
/// Return true if `shows table is populated.
pub fn is_podcasts_populated(filter_ids: &[i32]) -> Result<bool, DataError> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
select(exists(podcast.filter(id.ne_all(filter_ids))))
select(exists(shows.filter(id.ne_all(filter_ids))))
.get_result(&con)
.map_err(From::from)
}
pub(crate) fn index_new_episodes(eps: &[NewEpisode]) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(episode)
diesel::insert_into(episodes)
.values(eps)
.execute(&*con)
.map_err(From::from)
.map(|_| ())
}
pub fn update_none_to_played_now(parent: &Podcast) -> Result<usize, DataError> {
use schema::episode::dsl::*;
pub fn update_none_to_played_now(parent: &Show) -> Result<usize, DataError> {
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
@@ -5,12 +5,18 @@ use hyper;
use native_tls;
use rss;
use url;
use xml;
use std::io;
use models::Source;
#[fail(display = "Request to {} returned {}. Context: {}", url, status_code, context)]
#[fail(
display = "Request to {} returned {}. Context: {}",
url,
status_code,
context
)]
#[derive(Fail, Debug)]
pub struct HttpStatusError {
url: String,
@@ -49,13 +55,18 @@ pub enum DataError {
IOError(#[cause] io::Error),
#[fail(display = "RSS Error: {}", _0)]
RssError(#[cause] rss::Error),
#[fail(display = "XML Reader Error: {}", _0)]
XmlReaderError(#[cause] xml::reader::Error),
#[fail(display = "Error: {}", _0)]
Bail(String),
#[fail(display = "{}", _0)]
HttpStatusGeneral(HttpStatusError),
#[fail(display = "FIXME: This should be better")]
F301(Source),
#[fail(display = "Error occured while Parsing an Episode. Reason: {}", reason)]
#[fail(
display = "Error occured while Parsing an Episode. Reason: {}",
reason
)]
ParseEpisodeError { reason: String, parent_id: i32 },
#[fail(display = "Episode was not changed and thus skipped.")]
EpisodeNotChanged,
@@ -115,6 +126,12 @@ impl From<rss::Error> for DataError {
}
}
impl From<xml::reader::Error> for DataError {
fn from(err: xml::reader::Error) -> Self {
DataError::XmlReaderError(err)
}
}
impl From<String> for DataError {
fn from(err: String) -> Self {
DataError::Bail(err)
@@ -9,7 +9,7 @@ use rss;
use dbqueries;
use errors::DataError;
use models::{Index, IndexState, Update};
use models::{NewEpisode, NewEpisodeMinimal, NewPodcast, Podcast};
use models::{NewEpisode, NewEpisodeMinimal, NewShow, Show};
/// Wrapper struct that hold a `Source` id and the `rss::Channel`
/// that corresponds to the `Source.uri` field.
@@ -25,23 +25,21 @@ pub struct Feed {
impl Feed {
/// Index the contents of the RSS `Feed` into the database.
pub fn index(self) -> Box<Future<Item = (), Error = DataError> + Send> {
let fut = self.parse_podcast_async()
pub fn index(self) -> impl Future<Item = (), Error = DataError> + Send {
self.parse_podcast_async()
.and_then(|pd| pd.to_podcast())
.and_then(move |pd| self.index_channel_items(pd));
Box::new(fut)
.and_then(move |pd| self.index_channel_items(pd))
}
fn parse_podcast(&self) -> NewPodcast {
NewPodcast::new(&self.channel, self.source_id)
fn parse_podcast(&self) -> NewShow {
NewShow::new(&self.channel, self.source_id)
}
fn parse_podcast_async(&self) -> Box<Future<Item = NewPodcast, Error = DataError> + Send> {
Box::new(ok(self.parse_podcast()))
fn parse_podcast_async(&self) -> impl Future<Item = NewShow, Error = DataError> + Send {
ok(self.parse_podcast())
}
fn index_channel_items(self, pd: Podcast) -> Box<Future<Item = (), Error = DataError> + Send> {
fn index_channel_items(self, pd: Show) -> impl Future<Item = (), Error = DataError> + Send {
let stream = stream::iter_ok::<_, DataError>(self.channel.into_items());
// Parse the episodes
@@ -52,11 +50,9 @@ impl Feed {
});
// Filter errors, Index updatable episodes, return insertables.
let insertables = filter_episodes(episodes);
filter_episodes(episodes)
// Batch index insertable episodes.
let idx = insertables.and_then(|eps| ok(batch_insert_episodes(&eps)));
Box::new(idx)
.and_then(|eps| ok(batch_insert_episodes(&eps)))
}
}
@@ -69,12 +65,12 @@ fn determine_ep_state(
item: &rss::Item,
) -> Result<IndexState<NewEpisode>, DataError> {
// Check if feed exists
let exists = dbqueries::episode_exists(ep.title(), ep.podcast_id())?;
let exists = dbqueries::episode_exists(ep.title(), ep.show_id())?;
if !exists {
Ok(IndexState::Index(ep.into_new_episode(item)))
} else {
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.podcast_id())?;
let old = dbqueries::get_episode_minimal_from_pk(ep.title(), ep.show_id())?;
let rowid = old.rowid();
if ep != old {
@@ -87,11 +83,11 @@ fn determine_ep_state(
fn filter_episodes<'a, S>(
stream: S,
) -> Box<Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a>
) -> impl Future<Item = Vec<NewEpisode>, Error = DataError> + Send + 'a
where
S: Stream<Item = IndexState<NewEpisode>, Error = DataError> + Send + 'a,
{
let list = stream.filter_map(|state| match state {
stream.filter_map(|state| match state {
IndexState::NotChanged => None,
// Update individual rows, and filter them
IndexState::Update((ref ep, rowid)) => {
@@ -105,9 +101,7 @@ where
IndexState::Index(s) => Some(s),
})
// only Index is left, collect them for batch index
.collect();
Box::new(list)
.collect()
}
fn batch_insert_episodes(episodes: &[NewEpisode]) {
@@ -180,7 +174,8 @@ mod tests {
fn test_complete_index() {
truncate_db().unwrap();
let feeds: Vec<_> = URLS.iter()
let feeds: Vec<_> = URLS
.iter()
.map(|&(path, url)| {
// Create and insert a Source into db
let s = Source::from_url(url).unwrap();
@@ -209,7 +204,7 @@ mod tests {
let file = fs::File::open(path).unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(feed.parse_podcast(), pd);
}
+152
View File
@@ -0,0 +1,152 @@
#![recursion_limit = "1024"]
#![allow(unknown_lints)]
#![cfg_attr(
all(test, feature = "clippy"),
allow(option_unwrap_used, result_unwrap_used)
)]
#![cfg_attr(feature = "cargo-clippy", allow(option_map_unit_fn))]
#![cfg_attr(
feature = "clippy",
warn(
option_unwrap_used,
result_unwrap_used,
print_stdout,
wrong_pub_self_convention,
mut_mut,
non_ascii_literal,
similar_names,
unicode_not_nfc,
enum_glob_use,
if_not_else,
items_after_statements,
used_underscore_binding
)
)]
// Enable lint group collections
#![warn(
nonstandard_style,
edition_2018,
rust_2018_idioms,
bad_style,
unused
)]
// standalone lints
#![warn(
const_err,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
plugin_as_library,
private_no_mangle_fns,
private_no_mangle_statics,
unconditional_recursion,
unions_with_drop_fields,
while_true,
missing_debug_implementations,
missing_docs,
trivial_casts,
trivial_numeric_casts,
elided_lifetime_in_paths,
missing_copy_implementations
)]
#![deny(warnings)]
// warn when code is not using dyn Trait syntax. req rustc 1.27
// #![deny(bare_trait_object)]
//! FIXME: Docs
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
#[cfg(test)]
#[macro_use]
extern crate maplit;
#[macro_use]
extern crate derive_builder;
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
// #[macro_use]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate ammonia;
extern crate chrono;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate native_tls;
extern crate num_cpus;
extern crate rayon;
extern crate rayon_futures;
extern crate rfc822_sanitizer;
extern crate rss;
extern crate tokio_core;
extern crate url;
extern crate xdg;
extern crate xml;
pub mod database;
#[allow(missing_docs)]
pub mod dbqueries;
#[allow(missing_docs)]
pub mod errors;
mod feed;
pub(crate) mod models;
pub mod opml;
mod parser;
pub mod pipeline;
mod schema;
pub mod utils;
pub use feed::{Feed, FeedBuilder};
pub use models::Save;
pub use models::{Episode, EpisodeWidgetModel, Show, ShowCoverModel, Source};
// Set the user agent, See #53 for more
// Keep this in sync with Tor-browser releases
/// The user-agent to be used for all the requests.
/// It originates from the Tor-browser UA.
pub const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 6.1; rv:52.0) Gecko/20100101 Firefox/52.0";
/// [XDG Base Direcotory](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) Paths.
#[allow(missing_debug_implementations)]
pub mod xdg_dirs {
use std::path::PathBuf;
use xdg;
lazy_static!{
pub(crate) static ref PODCASTS_XDG: xdg::BaseDirectories = {
xdg::BaseDirectories::with_prefix("gnome-podcasts").unwrap()
};
/// XDG_DATA Directory `Pathbuf`.
pub static ref PODCASTS_DATA: PathBuf = {
PODCASTS_XDG.create_data_directory(PODCASTS_XDG.get_data_home()).unwrap()
};
/// XDG_CONFIG Directory `Pathbuf`.
pub static ref PODCASTS_CONFIG: PathBuf = {
PODCASTS_XDG.create_config_directory(PODCASTS_XDG.get_config_home()).unwrap()
};
/// XDG_CACHE Directory `Pathbuf`.
pub static ref PODCASTS_CACHE: PathBuf = {
PODCASTS_XDG.create_cache_directory(PODCASTS_XDG.get_cache_home()).unwrap()
};
/// GNOME Podcasts Download Direcotry `PathBuf`.
pub static ref DL_DIR: PathBuf = {
PODCASTS_XDG.create_data_directory("Downloads").unwrap()
};
}
}
@@ -5,14 +5,14 @@ use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Podcast, Save};
use schema::episode;
use models::{Save, Show};
use schema::episodes;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[belongs_to(Podcast, foreign_key = "podcast_id")]
#[primary_key(title, show_id)]
#[belongs_to(Show, foreign_key = "show_id")]
#[derive(Debug, Clone)]
/// Diesel Model of the episode table.
pub struct Episode {
@@ -26,9 +26,7 @@ pub struct Episode {
duration: Option<i32>,
guid: Option<String>,
played: Option<i32>,
favorite: bool,
archive: bool,
podcast_id: i32,
show_id: i32,
}
impl Save<Episode> for Episode {
@@ -154,32 +152,9 @@ impl Episode {
self.played = value;
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
///
/// If true, the download cleanr will ignore the episode
/// and the corresponding media value will never be automaticly deleted.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Episode`.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
/// Sets the `played` value with the current `epoch` timestap and save it.
@@ -191,12 +166,12 @@ impl Episode {
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for constructing `EpisodeWidgets`.
pub struct EpisodeWidgetQuery {
pub struct EpisodeWidgetModel {
rowid: i32,
title: String,
uri: Option<String>,
@@ -205,14 +180,12 @@ pub struct EpisodeWidgetQuery {
length: Option<i32>,
duration: Option<i32>,
played: Option<i32>,
// favorite: bool,
// archive: bool,
podcast_id: i32,
show_id: i32,
}
impl From<Episode> for EpisodeWidgetQuery {
fn from(e: Episode) -> EpisodeWidgetQuery {
EpisodeWidgetQuery {
impl From<Episode> for EpisodeWidgetModel {
fn from(e: Episode) -> EpisodeWidgetModel {
EpisodeWidgetModel {
rowid: e.rowid,
title: e.title,
uri: e.uri,
@@ -221,30 +194,30 @@ impl From<Episode> for EpisodeWidgetQuery {
length: e.length,
duration: e.duration,
played: e.played,
podcast_id: e.podcast_id,
show_id: e.show_id,
}
}
}
impl Save<usize> for EpisodeWidgetQuery {
impl Save<usize> for EpisodeWidgetModel {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episode.filter(rowid.eq(self.rowid)))
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl EpisodeWidgetQuery {
impl EpisodeWidgetModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
@@ -319,32 +292,9 @@ impl EpisodeWidgetQuery {
self.played = value;
}
// /// Represents the archiving policy for the episode.
// pub fn archive(&self) -> bool {
// self.archive
// }
// /// Set the `archive` policy.
// ///
// /// If true, the download cleanr will ignore the episode
// /// and the corresponding media value will never be automaticly deleted.
// pub fn set_archive(&mut self, b: bool) {
// self.archive = b
// }
// /// Get the `favorite` status of the `Episode`.
// pub fn favorite(&self) -> bool {
// self.favorite
// }
// /// Set `favorite` status.
// pub fn set_favorite(&mut self, b: bool) {
// self.favorite = b
// }
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
/// Sets the `played` value with the current `epoch` timestap and save it.
@@ -356,38 +306,38 @@ impl EpisodeWidgetQuery {
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used internal with the `utils::checkup` function.
pub struct EpisodeCleanerQuery {
pub struct EpisodeCleanerModel {
rowid: i32,
local_uri: Option<String>,
played: Option<i32>,
}
impl Save<usize> for EpisodeCleanerQuery {
impl Save<usize> for EpisodeCleanerModel {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<usize, Self::Error> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let tempdb = db.get()?;
diesel::update(episode.filter(rowid.eq(self.rowid)))
diesel::update(episodes.filter(rowid.eq(self.rowid)))
.set(self)
.execute(&*tempdb)
.map_err(From::from)
}
}
impl From<Episode> for EpisodeCleanerQuery {
fn from(e: Episode) -> EpisodeCleanerQuery {
EpisodeCleanerQuery {
impl From<Episode> for EpisodeCleanerModel {
fn from(e: Episode) -> EpisodeCleanerModel {
EpisodeCleanerModel {
rowid: e.rowid(),
local_uri: e.local_uri,
played: e.played,
@@ -395,7 +345,7 @@ impl From<Episode> for EpisodeCleanerQuery {
}
}
impl EpisodeCleanerQuery {
impl EpisodeCleanerModel {
/// Get the value of the sqlite's `ROW_ID`
pub fn rowid(&self) -> i32 {
self.rowid
@@ -428,9 +378,9 @@ impl EpisodeCleanerQuery {
}
#[derive(Queryable, AsChangeset, PartialEq)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[changeset_options(treat_none_as_null = "true")]
#[primary_key(title, podcast_id)]
#[primary_key(title, show_id)]
#[derive(Debug, Clone)]
/// Diesel Model to be used for FIXME.
pub struct EpisodeMinimal {
@@ -438,9 +388,10 @@ pub struct EpisodeMinimal {
title: String,
uri: Option<String>,
epoch: i32,
length: Option<i32>,
duration: Option<i32>,
guid: Option<String>,
podcast_id: i32,
show_id: i32,
}
impl From<Episode> for EpisodeMinimal {
@@ -449,10 +400,11 @@ impl From<Episode> for EpisodeMinimal {
rowid: e.rowid,
title: e.title,
uri: e.uri,
length: e.length,
guid: e.guid,
epoch: e.epoch,
duration: e.duration,
podcast_id: e.podcast_id,
show_id: e.show_id,
}
}
}
@@ -488,6 +440,18 @@ impl EpisodeMinimal {
self.epoch
}
/// Get the `length`.
///
/// The number represents the size of the file in bytes.
pub fn length(&self) -> Option<i32> {
self.length
}
/// Set the `length`.
pub fn set_length(&mut self, value: Option<i32>) {
self.length = value;
}
/// Get the `duration` value.
///
/// The number represents the duration of the item/episode in seconds.
@@ -495,8 +459,8 @@ impl EpisodeMinimal {
self.duration
}
/// `Podcast` table foreign key.
pub fn podcast_id(&self) -> i32 {
self.podcast_id
/// `Show` table foreign key.
pub fn show_id(&self) -> i32 {
self.show_id
}
}
@@ -1,26 +1,26 @@
mod new_episode;
mod new_podcast;
mod new_show;
mod new_source;
mod episode;
mod podcast;
mod show;
mod source;
// use futures::prelude::*;
// use futures::future::*;
pub(crate) use self::episode::EpisodeCleanerQuery;
pub(crate) use self::episode::EpisodeCleanerModel;
pub(crate) use self::new_episode::{NewEpisode, NewEpisodeMinimal};
pub(crate) use self::new_podcast::NewPodcast;
pub(crate) use self::new_show::NewShow;
pub(crate) use self::new_source::NewSource;
#[cfg(test)]
pub(crate) use self::new_episode::NewEpisodeBuilder;
#[cfg(test)]
pub(crate) use self::new_podcast::NewPodcastBuilder;
pub(crate) use self::new_show::NewShowBuilder;
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetQuery};
pub use self::podcast::{Podcast, PodcastCoverQuery};
pub use self::episode::{Episode, EpisodeMinimal, EpisodeWidgetModel};
pub use self::show::{Show, ShowCoverModel};
pub use self::source::Source;
#[derive(Debug, Clone, PartialEq)]
@@ -9,11 +9,11 @@ use dbqueries;
use errors::DataError;
use models::{Episode, EpisodeMinimal, Index, Insert, Update};
use parser;
use schema::episode;
use schema::episodes;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
@@ -26,7 +26,7 @@ pub(crate) struct NewEpisode {
duration: Option<i32>,
guid: Option<String>,
epoch: i32,
podcast_id: i32,
show_id: i32,
}
impl From<NewEpisodeMinimal> for NewEpisode {
@@ -36,7 +36,7 @@ impl From<NewEpisodeMinimal> for NewEpisode {
.uri(e.uri)
.duration(e.duration)
.epoch(e.epoch)
.podcast_id(e.podcast_id)
.show_id(e.show_id)
.guid(e.guid)
.build()
.unwrap()
@@ -47,12 +47,12 @@ impl Insert<()> for NewEpisode {
type Error = DataError;
fn insert(&self) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Inserting {:?}", self.title);
diesel::insert_into(episode)
diesel::insert_into(episodes)
.values(self)
.execute(&con)
.map_err(From::from)
@@ -64,12 +64,12 @@ impl Update<()> for NewEpisode {
type Error = DataError;
fn update(&self, episode_id: i32) -> Result<(), DataError> {
use schema::episode::dsl::*;
use schema::episodes::dsl::*;
let db = connection();
let con = db.get()?;
info!("Updating {:?}", self.title);
diesel::update(episode.filter(rowid.eq(episode_id)))
diesel::update(episodes.filter(rowid.eq(episode_id)))
.set(self)
.execute(&con)
.map_err(From::from)
@@ -83,10 +83,10 @@ impl Index<()> for NewEpisode {
// Does not update the episode description if it's the only thing that has
// changed.
fn index(&self) -> Result<(), DataError> {
let exists = dbqueries::episode_exists(self.title(), self.podcast_id())?;
let exists = dbqueries::episode_exists(self.title(), self.show_id())?;
if exists {
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.podcast_id())?;
let other = dbqueries::get_episode_minimal_from_pk(self.title(), self.show_id())?;
if self != &other {
self.update(other.rowid())
@@ -101,17 +101,23 @@ impl Index<()> for NewEpisode {
impl PartialEq<EpisodeMinimal> for NewEpisode {
fn eq(&self, other: &EpisodeMinimal) -> bool {
(self.title() == other.title()) && (self.uri() == other.uri())
&& (self.duration() == other.duration()) && (self.epoch() == other.epoch())
&& (self.guid() == other.guid()) && (self.podcast_id() == other.podcast_id())
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
}
}
impl PartialEq<Episode> for NewEpisode {
fn eq(&self, other: &Episode) -> bool {
(self.title() == other.title()) && (self.uri() == other.uri())
&& (self.duration() == other.duration()) && (self.epoch() == other.epoch())
&& (self.guid() == other.guid()) && (self.podcast_id() == other.podcast_id())
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
&& (self.description() == other.description())
&& (self.length() == other.length())
}
@@ -120,14 +126,14 @@ impl PartialEq<Episode> for NewEpisode {
impl NewEpisode {
/// Parses an `rss::Item` into a `NewEpisode` Struct.
#[allow(dead_code)]
pub(crate) fn new(item: &rss::Item, podcast_id: i32) -> Result<Self, DataError> {
NewEpisodeMinimal::new(item, podcast_id).map(|ep| ep.into_new_episode(item))
pub(crate) fn new(item: &rss::Item, show_id: i32) -> Result<Self, DataError> {
NewEpisodeMinimal::new(item, show_id).map(|ep| ep.into_new_episode(item))
}
#[allow(dead_code)]
pub(crate) fn to_episode(&self) -> Result<Episode, DataError> {
self.index()?;
dbqueries::get_episode_from_pk(&self.title, self.podcast_id).map_err(From::from)
dbqueries::get_episode_from_pk(&self.title, self.show_id).map_err(From::from)
}
}
@@ -161,30 +167,34 @@ impl NewEpisode {
self.length
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[derive(Insertable, AsChangeset)]
#[table_name = "episode"]
#[table_name = "episodes"]
#[derive(Debug, Clone, Builder, PartialEq)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewEpisodeMinimal {
title: String,
uri: Option<String>,
length: Option<i32>,
duration: Option<i32>,
epoch: i32,
guid: Option<String>,
podcast_id: i32,
show_id: i32,
}
impl PartialEq<EpisodeMinimal> for NewEpisodeMinimal {
fn eq(&self, other: &EpisodeMinimal) -> bool {
(self.title() == other.title()) && (self.uri() == other.uri())
&& (self.duration() == other.duration()) && (self.epoch() == other.epoch())
&& (self.guid() == other.guid()) && (self.podcast_id() == other.podcast_id())
(self.title() == other.title())
&& (self.uri() == other.uri())
&& (self.duration() == other.duration())
&& (self.epoch() == other.epoch())
&& (self.guid() == other.guid())
&& (self.show_id() == other.show_id())
}
}
@@ -202,12 +212,19 @@ impl NewEpisodeMinimal {
let title = item.title().unwrap().trim().to_owned();
let guid = item.guid().map(|s| s.value().trim().to_owned());
let uri = item.enclosure()
.map(|s| url_cleaner(s.url().trim()))
// Get the mime type, the `http` url and the length from the enclosure
// http://www.rssboard.org/rss-specification#ltenclosuregtSubelementOfLtitemgt
let enc = item.enclosure();
// Get the url
let uri = enc.map(|s| url_cleaner(s.url().trim()))
// Fallback to Rss.Item.link if enclosure is None.
.or_else(|| item.link().map(|s| url_cleaner(s.trim())));
// If url is still None return an Error as this behaviour is
// Get the size of the content, it should be in bytes
let length = enc.and_then(|x| x.length().parse().ok());
// If url is still None return an Error as this behaviour is not
// compliant with the RSS Spec.
if uri.is_none() {
let err = DataError::ParseEpisodeError {
@@ -229,17 +246,18 @@ impl NewEpisodeMinimal {
NewEpisodeMinimalBuilder::default()
.title(title)
.uri(uri)
.length(length)
.duration(duration)
.epoch(epoch)
.guid(guid)
.podcast_id(parent_id)
.show_id(parent_id)
.build()
.map_err(From::from)
}
// TODO: TryInto is stabilizing in rustc v1.26!
// ^ Jokes on you past self!
pub(crate) fn into_new_episode(self, item: &rss::Item) -> NewEpisode {
let length = item.enclosure().and_then(|x| x.length().parse().ok());
let description = item.description().and_then(|s| {
let sanitized_html = ammonia::Builder::new()
// Remove `rel` attributes from `<a>` tags
@@ -254,9 +272,9 @@ impl NewEpisodeMinimal {
.uri(self.uri)
.duration(self.duration)
.epoch(self.epoch)
.podcast_id(self.podcast_id)
.show_id(self.show_id)
.guid(self.guid)
.length(length)
.length(self.length)
.description(description)
.build()
.unwrap()
@@ -285,10 +303,11 @@ impl NewEpisodeMinimal {
self.epoch
}
pub(crate) fn podcast_id(&self) -> i32 {
self.podcast_id
pub(crate) fn show_id(&self) -> i32 {
self.show_id
}
}
#[cfg(test)]
mod tests {
use database::truncate_db;
@@ -314,8 +333,9 @@ mod tests {
)))
.guid(Some(String::from("7df4070a-9832-11e7-adac-cb37b05d5e24")))
.epoch(1505296800)
.length(Some(66738886))
.duration(Some(4171))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -327,8 +347,9 @@ mod tests {
)))
.guid(Some(String::from("7c207a24-e33f-11e6-9438-eb45dcf36a1d")))
.epoch(1502272800)
.length(Some(67527575))
.duration(Some(4415))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -349,7 +370,7 @@ mod tests {
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(4171))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -373,7 +394,7 @@ mod tests {
.length(Some(67527575))
.epoch(1502272800)
.duration(Some(4415))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -388,7 +409,7 @@ mod tests {
.length(Some(66738886))
.epoch(1505296800)
.duration(Some(424242))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -399,9 +420,10 @@ mod tests {
"http://www.podtrac.com/pts/redirect.mp3/traffic.libsyn.com/jnite/lup-0214.mp3",
)))
.guid(Some(String::from("78A682B4-73E8-47B8-88C0-1BE62DD4EF9D")))
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -413,8 +435,9 @@ mod tests {
)))
.guid(Some(String::from("1CE57548-B36C-4F14-832A-5D5E0A24E35B")))
.epoch(1504670247)
.length(Some(36544272))
.duration(Some(4491))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -434,7 +457,7 @@ mod tests {
.length(Some(46479789))
.epoch(1505280282)
.duration(Some(5733))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -456,7 +479,7 @@ mod tests {
.length(Some(36544272))
.epoch(1504670247)
.duration(Some(4491))
.podcast_id(42)
.show_id(42)
.build()
.unwrap()
};
@@ -530,6 +553,10 @@ mod tests {
let ep = EXPECTED_MINIMAL_INTERCEPTED_1
.clone()
.into_new_episode(&item);
println!(
"EPISODE: {:#?}\nEXPECTED: {:#?}",
ep, *EXPECTED_INTERCEPTED_1
);
assert_eq!(ep, *EXPECTED_INTERCEPTED_1);
let item = channel.items().iter().nth(15).unwrap();
@@ -549,7 +576,7 @@ mod tests {
let episode = channel.items().iter().nth(14).unwrap();
let new_ep = NewEpisode::new(&episode, 42).unwrap();
new_ep.insert().unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id()).unwrap();
assert_eq!(new_ep, ep);
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_1);
@@ -558,7 +585,7 @@ mod tests {
let episode = channel.items().iter().nth(15).unwrap();
let new_ep = NewEpisode::new(&episode, 42).unwrap();
new_ep.insert().unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.podcast_id()).unwrap();
let ep = dbqueries::get_episode_from_pk(new_ep.title(), new_ep.show_id()).unwrap();
assert_eq!(new_ep, ep);
assert_eq!(&new_ep, &*EXPECTED_INTERCEPTED_2);
@@ -572,21 +599,15 @@ mod tests {
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
updated.update(old.rowid()).unwrap();
let mut new = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
let new = dbqueries::get_episode_from_pk(old.title(), old.show_id()).unwrap();
// Assert that updating does not change the rowid and podcast_id
// Assert that updating does not change the rowid and show_id
assert_ne!(old, new);
assert_eq!(old.rowid(), new.rowid());
assert_eq!(old.podcast_id(), new.podcast_id());
assert_eq!(old.show_id(), new.show_id());
assert_eq!(updated, &new);
assert_ne!(updated, &old);
new.set_archive(true);
new.save().unwrap();
let new2 = dbqueries::get_episode_from_pk(old.title(), old.podcast_id()).unwrap();
assert_eq!(true, new2.archive());
}
#[test]
@@ -599,7 +620,7 @@ mod tests {
// Second identical, This should take the early return path
assert!(expected.index().is_ok());
// Get the episode
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
// Assert that NewPodcast is equal to the Indexed one
assert_eq!(*expected, old);
@@ -608,23 +629,22 @@ mod tests {
// Update the podcast
assert!(updated.index().is_ok());
// Get the new Podcast
let new = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let new = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
// Assert it's diff from the old one.
assert_ne!(new, old);
assert_eq!(*updated, new);
assert_eq!(new.rowid(), old.rowid());
assert_eq!(new.podcast_id(), old.podcast_id());
assert_eq!(new.show_id(), old.show_id());
}
#[test]
fn test_new_episode_to_episode() {
let expected = &*EXPECTED_INTERCEPTED_1;
let updated = &*UPDATED_DURATION_INTERCEPTED_1;
// Assert insert() produces the same result that you would get with to_podcast()
truncate_db().unwrap();
expected.insert().unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
let ep = expected.to_episode().unwrap();
assert_eq!(old, ep);
@@ -633,17 +653,7 @@ mod tests {
let ep = expected.to_episode().unwrap();
// This should error as a unique constrain violation
assert!(expected.insert().is_err());
let mut old =
dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.show_id()).unwrap();
assert_eq!(old, ep);
old.set_archive(true);
old.save().unwrap();
// Assert that it does not mess with user preferences
let ep = updated.to_episode().unwrap();
let old = dbqueries::get_episode_from_pk(expected.title(), expected.podcast_id()).unwrap();
assert_eq!(old, ep);
assert_eq!(old.archive(), true);
}
}
@@ -4,21 +4,21 @@ use diesel::prelude::*;
use rss;
use errors::DataError;
use models::Podcast;
use models::Show;
use models::{Index, Insert, Update};
use schema::podcast;
use schema::shows;
use database::connection;
use dbqueries;
use utils::url_cleaner;
#[derive(Insertable, AsChangeset)]
#[table_name = "podcast"]
#[table_name = "shows"]
#[derive(Debug, Clone, Default, Builder, PartialEq)]
#[builder(default)]
#[builder(derive(Debug))]
#[builder(setter(into))]
pub(crate) struct NewPodcast {
pub(crate) struct NewShow {
title: String,
link: String,
description: String,
@@ -26,15 +26,15 @@ pub(crate) struct NewPodcast {
source_id: i32,
}
impl Insert<()> for NewPodcast {
impl Insert<()> for NewShow {
type Error = DataError;
fn insert(&self) -> Result<(), Self::Error> {
use schema::podcast::dsl::*;
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
diesel::insert_into(podcast)
diesel::insert_into(shows)
.values(self)
.execute(&con)
.map(|_| ())
@@ -42,16 +42,16 @@ impl Insert<()> for NewPodcast {
}
}
impl Update<()> for NewPodcast {
impl Update<()> for NewShow {
type Error = DataError;
fn update(&self, podcast_id: i32) -> Result<(), Self::Error> {
use schema::podcast::dsl::*;
fn update(&self, show_id: i32) -> Result<(), Self::Error> {
use schema::shows::dsl::*;
let db = connection();
let con = db.get()?;
info!("Updating {}", self.title);
diesel::update(podcast.filter(id.eq(podcast_id)))
diesel::update(shows.filter(id.eq(show_id)))
.set(self)
.execute(&con)
.map(|_| ())
@@ -61,7 +61,7 @@ impl Update<()> for NewPodcast {
// TODO: Maybe return an Enum<Action(Resut)> Instead.
// It would make unti testing better too.
impl Index<()> for NewPodcast {
impl Index<()> for NewShow {
type Error = DataError;
fn index(&self) -> Result<(), DataError> {
@@ -81,18 +81,19 @@ impl Index<()> for NewPodcast {
}
}
impl PartialEq<Podcast> for NewPodcast {
fn eq(&self, other: &Podcast) -> bool {
(self.link() == other.link()) && (self.title() == other.title())
impl PartialEq<Show> for NewShow {
fn eq(&self, other: &Show) -> bool {
(self.link() == other.link())
&& (self.title() == other.title())
&& (self.image_uri() == other.image_uri())
&& (self.description() == other.description())
&& (self.source_id() == other.source_id())
}
}
impl NewPodcast {
/// Parses a `rss::Channel` into a `NewPodcast` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewPodcast {
impl NewShow {
/// Parses a `rss::Channel` into a `NewShow` Struct.
pub(crate) fn new(chan: &rss::Channel, source_id: i32) -> NewShow {
let title = chan.title().trim();
let link = url_cleaner(chan.link().trim());
@@ -103,13 +104,14 @@ impl NewPodcast {
.to_string();
// Try to get the itunes img first
let itunes_img = chan.itunes_ext()
let itunes_img = chan
.itunes_ext()
.and_then(|s| s.image().map(|url| url.trim()))
.map(|s| s.to_owned());
// If itunes is None, try to get the channel.image from the rss spec
let image_uri = itunes_img.or_else(|| chan.image().map(|s| s.url().trim().to_owned()));
NewPodcastBuilder::default()
NewShowBuilder::default()
.title(title)
.description(description)
.link(link)
@@ -120,14 +122,14 @@ impl NewPodcast {
}
// Look out for when tryinto lands into stable.
pub(crate) fn to_podcast(&self) -> Result<Podcast, DataError> {
pub(crate) fn to_podcast(&self) -> Result<Show, DataError> {
self.index()?;
dbqueries::get_podcast_from_source_id(self.source_id).map_err(From::from)
}
}
// Ignore the following geters. They are used in unit tests mainly.
impl NewPodcast {
impl NewShow {
#[allow(dead_code)]
pub(crate) fn source_id(&self) -> i32 {
self.source_id
@@ -158,14 +160,14 @@ mod tests {
use rss::Channel;
use database::truncate_db;
use models::{NewPodcastBuilder, Save};
use models::NewShowBuilder;
use std::fs::File;
use std::io::BufReader;
// Pre-built expected NewPodcast structs.
// Pre-built expected NewShow structs.
lazy_static! {
static ref EXPECTED_INTERCEPTED: NewPodcast = {
static ref EXPECTED_INTERCEPTED: NewShow = {
let descr = "The people behind The Intercepts fearless reporting and incisive \
commentaryJeremy Scahill, Glenn Greenwald, Betsy Reed and \
othersdiscuss the crucial issues of our time: national security, civil \
@@ -173,7 +175,7 @@ mod tests {
artists, thinkers, and newsmakers who challenge our preconceptions about \
the world we live in.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description(descr)
@@ -186,12 +188,12 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_LUP: NewPodcast = {
static ref EXPECTED_LUP: NewShow = {
let descr = "An open show powered by community LINUX Unplugged takes the best \
attributes of open collaboration and focuses them into a weekly \
lifestyle show about Linux.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("LINUX Unplugged Podcast")
.link("http://www.jupiterbroadcasting.com/")
.description(descr)
@@ -202,7 +204,7 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_TIPOFF: NewPodcast = {
static ref EXPECTED_TIPOFF: NewShow = {
let desc = "<p>Welcome to The Tip Off- the podcast where we take you behind the \
scenes of some of the best investigative journalism from recent years. \
Each episode well be digging into an investigative scoop- hearing from \
@@ -213,7 +215,7 @@ mod tests {
complicated detective work that goes into doing great investigative \
journalism- then this is the podcast for you.</p>";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("The Tip Off")
.link("http://www.acast.com/thetipoff")
.description(desc)
@@ -225,7 +227,7 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_STARS: NewPodcast = {
static ref EXPECTED_STARS: NewShow = {
let descr = "<p>The first audio drama from Tor Labs and Gideon Media, Steal the Stars \
is a gripping noir science fiction thriller in 14 episodes: Forbidden \
love, a crashed UFO, an alien body, and an impossible heist unlike any \
@@ -235,7 +237,7 @@ mod tests {
b183-7311d2e436c3/b3a4aa57a576bb662191f2a6bc2a436c8c4ae256ecffaff5c4c54fd42e\
923914941c264d01efb1833234b52c9530e67d28a8cebbe3d11a4bc0fbbdf13ecdf1c3.jpeg";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Steal the Stars")
.link("http://tor-labs.com/")
.description(descr)
@@ -244,12 +246,12 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_CODE: NewPodcast = {
static ref EXPECTED_CODE: NewShow = {
let descr = "A podcast about humans and technology. Panelists: Coraline Ada Ehmke, \
David Brady, Jessica Kerr, Jay Bobo, Astrid Countee and Sam \
Livingston-Gray. Brought to you by @therubyrep.";
NewPodcastBuilder::default()
NewShowBuilder::default()
.title("Greater Than Code")
.link("https://www.greaterthancode.com/")
.description(descr)
@@ -260,8 +262,8 @@ mod tests {
.build()
.unwrap()
};
static ref EXPECTED_ELLINOFRENEIA: NewPodcast = {
NewPodcastBuilder::default()
static ref EXPECTED_ELLINOFRENEIA: NewShow = {
NewShowBuilder::default()
.title("Ελληνοφρένεια")
.link("https://ellinofreneia.sealabs.net/feed.rss")
.description("Ανεπίσημο feed της Ελληνοφρένειας")
@@ -270,8 +272,8 @@ mod tests {
.build()
.unwrap()
};
static ref UPDATED_DESC_INTERCEPTED: NewPodcast = {
NewPodcastBuilder::default()
static ref UPDATED_DESC_INTERCEPTED: NewShow = {
NewShowBuilder::default()
.title("Intercepted with Jeremy Scahill")
.link("https://theintercept.com/podcasts")
.description("New Description")
@@ -291,7 +293,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_INTERCEPTED, pd);
}
@@ -300,7 +302,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-LinuxUnplugged.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_LUP, pd);
}
@@ -309,7 +311,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-TheTipOff.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_TIPOFF, pd);
}
@@ -318,7 +320,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-StealTheStars.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_STARS, pd);
}
@@ -327,7 +329,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-GreaterThanCode.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_CODE, pd);
}
@@ -336,7 +338,7 @@ mod tests {
let file = File::open("tests/feeds/2018-03-28-Ellinofreneia.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let pd = NewPodcast::new(&channel, 42);
let pd = NewShow::new(&channel, 42);
assert_eq!(*EXPECTED_ELLINOFRENEIA, pd);
}
@@ -347,7 +349,7 @@ mod tests {
let file = File::open("tests/feeds/2018-01-20-Intercepted.xml").unwrap();
let channel = Channel::read_from(BufReader::new(file)).unwrap();
let npd = NewPodcast::new(&channel, 42);
let npd = NewShow::new(&channel, 42);
npd.insert().unwrap();
let pd = dbqueries::get_podcast_from_source_id(42).unwrap();
@@ -367,20 +369,13 @@ mod tests {
let updated = &*UPDATED_DESC_INTERCEPTED;
updated.update(old.id()).unwrap();
let mut new = dbqueries::get_podcast_from_source_id(42).unwrap();
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_ne!(old, new);
assert_eq!(old.id(), new.id());
assert_eq!(old.source_id(), new.source_id());
assert_eq!(updated, &new);
assert_ne!(updated, &old);
// Chech that the update does not override user preferences.
new.set_archive(true);
new.save().unwrap();
let new2 = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(true, new2.archive());
}
#[test]
@@ -393,14 +388,14 @@ mod tests {
assert!(EXPECTED_INTERCEPTED.index().is_ok());
// Get the podcast
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
// Assert that NewPodcast is equal to the Indexed one
// Assert that NewShow is equal to the Indexed one
assert_eq!(&*EXPECTED_INTERCEPTED, &old);
let updated = &*UPDATED_DESC_INTERCEPTED;
// Update the podcast
assert!(updated.index().is_ok());
// Get the new Podcast
// Get the new Show
let new = dbqueries::get_podcast_from_source_id(42).unwrap();
// Assert it's diff from the old one.
assert_ne!(new, old);
@@ -422,16 +417,7 @@ mod tests {
let pd = EXPECTED_INTERCEPTED.to_podcast().unwrap();
// This should error as a unique constrain violation
assert!(EXPECTED_INTERCEPTED.insert().is_err());
let mut old = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(old, pd);
old.set_archive(true);
old.save().unwrap();
// Assert that it does not mess with user preferences
let pd = UPDATED_DESC_INTERCEPTED.to_podcast().unwrap();
let old = dbqueries::get_podcast_from_source_id(42).unwrap();
assert_eq!(old, pd);
assert_eq!(old.archive(), true);
}
}
@@ -3,42 +3,37 @@ use diesel::SaveChangesDsl;
use database::connection;
use errors::DataError;
use models::{Save, Source};
use schema::podcast;
use std::sync::Arc;
use schema::shows;
#[derive(Queryable, Identifiable, AsChangeset, Associations, PartialEq)]
#[belongs_to(Source, foreign_key = "source_id")]
#[changeset_options(treat_none_as_null = "true")]
#[table_name = "podcast"]
#[table_name = "shows"]
#[derive(Debug, Clone)]
/// Diesel Model of the podcast table.
pub struct Podcast {
/// Diesel Model of the shows table.
pub struct Show {
id: i32,
title: String,
link: String,
description: String,
image_uri: Option<String>,
favorite: bool,
archive: bool,
always_dl: bool,
source_id: i32,
}
impl Save<Podcast> for Podcast {
impl Save<Show> for Show {
type Error = DataError;
/// Helper method to easily save/"sync" current state of self to the
/// Database.
fn save(&self) -> Result<Podcast, Self::Error> {
fn save(&self) -> Result<Show, Self::Error> {
let db = connection();
let tempdb = db.get()?;
self.save_changes::<Podcast>(&*tempdb).map_err(From::from)
self.save_changes::<Show>(&*tempdb).map_err(From::from)
}
}
impl Podcast {
impl Show {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
@@ -56,7 +51,7 @@ impl Podcast {
&self.link
}
/// Set the Podcast/Feed `link`.
/// Set the Show/Feed `link`.
pub fn set_link(&mut self, value: &str) {
self.link = value.to_string();
}
@@ -83,41 +78,6 @@ impl Podcast {
self.image_uri = value.map(|x| x.to_string());
}
/// Represents the archiving policy for the episode.
pub fn archive(&self) -> bool {
self.archive
}
/// Set the `archive` policy.
pub fn set_archive(&mut self, b: bool) {
self.archive = b
}
/// Get the `favorite` status of the `Podcast` Feed.
pub fn favorite(&self) -> bool {
self.favorite
}
/// Set `favorite` status.
pub fn set_favorite(&mut self, b: bool) {
self.favorite = b
}
/// Represents the download policy for the `Podcast` Feed.
///
/// Reserved for the use with a Download manager, yet to be implemented.
///
/// If true Podcast Episode should be downloaded automaticly/skipping
/// the selection queue.
pub fn always_download(&self) -> bool {
self.always_dl
}
/// Set the download policy.
pub fn set_always_download(&mut self, b: bool) {
self.always_dl = b
}
/// `Source` table foreign key.
pub fn source_id(&self) -> i32 {
self.source_id
@@ -125,17 +85,17 @@ impl Podcast {
}
#[derive(Queryable, Debug, Clone)]
/// Diesel Model of the podcast cover query.
/// Used for fetching information about a Podcast's cover.
pub struct PodcastCoverQuery {
/// Diesel Model of the Show cover query.
/// Used for fetching information about a Show's cover.
pub struct ShowCoverModel {
id: i32,
title: String,
image_uri: Option<String>,
}
impl From<Podcast> for PodcastCoverQuery {
fn from(p: Podcast) -> PodcastCoverQuery {
PodcastCoverQuery {
impl From<Show> for ShowCoverModel {
fn from(p: Show) -> ShowCoverModel {
ShowCoverModel {
id: p.id(),
title: p.title,
image_uri: p.image_uri,
@@ -143,17 +103,7 @@ impl From<Podcast> for PodcastCoverQuery {
}
}
impl From<Arc<Podcast>> for PodcastCoverQuery {
fn from(p: Arc<Podcast>) -> PodcastCoverQuery {
PodcastCoverQuery {
id: p.id(),
title: p.title.clone(),
image_uri: p.image_uri.clone(),
}
}
}
impl PodcastCoverQuery {
impl ShowCoverModel {
/// Get the Feed `id`.
pub fn id(&self) -> i32 {
self.id
@@ -4,8 +4,9 @@ use rss::Channel;
use url::Url;
use hyper::client::HttpConnector;
use hyper::header::{ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified,
Location, UserAgent};
use hyper::header::{
ETag, EntityTag, HttpDate, IfModifiedSince, IfNoneMatch, LastModified, Location, UserAgent,
};
use hyper::{Client, Method, Request, Response, StatusCode, Uri};
use hyper_tls::HttpsConnector;
@@ -177,7 +178,7 @@ impl Source {
self,
client: Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> Box<Future<Item = Feed, Error = DataError>> {
) -> impl Future<Item = Feed, Error = DataError> {
let id = self.id();
let response = loop_fn(self, move |source| {
source
@@ -194,7 +195,7 @@ impl Source {
})
});
let feed = response
response
.and_then(response_to_channel)
.and_then(move |chan| {
FeedBuilder::default()
@@ -202,9 +203,7 @@ impl Source {
.source_id(id)
.build()
.map_err(From::from)
});
Box::new(feed)
})
}
// TODO: make ignore_etags an Enum for better ergonomics.
@@ -213,7 +212,7 @@ impl Source {
self,
client: &Client<HttpsConnector<HttpConnector>>,
ignore_etags: bool,
) -> Box<Future<Item = Response, Error = DataError>> {
) -> impl Future<Item = Response, Error = DataError> {
// FIXME: remove unwrap somehow
let uri = Uri::from_str(self.uri()).unwrap();
let mut req = Request::new(Method::Get, uri);
@@ -234,25 +233,22 @@ impl Source {
}
}
let work = client
client
.request(req)
.map_err(From::from)
.and_then(move |res| self.match_status(res));
Box::new(work)
.and_then(move |res| self.match_status(res))
}
}
#[allow(needless_pass_by_value)]
fn response_to_channel(res: Response) -> Box<Future<Item = Channel, Error = DataError> + Send> {
let chan = res.body()
fn response_to_channel(res: Response) -> impl Future<Item = Channel, Error = DataError> + Send {
res.body()
.concat2()
.map(|x| x.into_iter())
.map_err(From::from)
.map(|iter| iter.collect::<Vec<u8>>())
.map(|utf_8_bytes| String::from_utf8_lossy(&utf_8_bytes).into_owned())
.and_then(|buf| Channel::from_str(&buf).map_err(From::from));
Box::new(chan)
.and_then(|buf| Channel::from_str(&buf).map_err(From::from))
}
#[cfg(test)]
+167
View File
@@ -0,0 +1,167 @@
//! FIXME: Docs
// #![allow(unused)]
use errors::DataError;
use models::Source;
use xml::reader;
use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::Path;
// use std::fs::{File, OpenOptions};
// use std::io::BufReader;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
// FIXME: Make it a Diesel model
/// Represents an `outline` xml element as per the `OPML` [specification][spec]
/// not `RSS` related sub-elements are ommited.
///
/// [spec]: http://dev.opml.org/spec2.html
pub struct Opml {
title: String,
description: String,
url: String,
}
/// Import feed url's from a `R` into the `Source` table.
// TODO: Write test
pub fn import_to_db<R: Read>(reader: R) -> Result<Vec<Source>, reader::Error> {
let feeds = extract_sources(reader)?
.iter()
.map(|opml| Source::from_url(&opml.url))
.filter_map(|s| {
if let Err(ref err) = s {
let txt = "If you think this might be a bug please consider filling a report over \
at https://gitlab.gnome.org/World/podcasts/issues/new";
error!("Failed to import a Show: {}", err);
error!("{}", txt);
}
s.ok()
})
.collect();
Ok(feeds)
}
/// Open a File from `P`, try to parse the OPML then insert the Feeds in the database and
/// return the new `Source`s
// TODO: Write test
pub fn import_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<Source>, DataError> {
let content = fs::read(path)?;
import_to_db(content.as_slice()).map_err(From::from)
}
/// Extracts the `outline` elemnts from a reader `R` and returns a `HashSet` of `Opml` structs.
pub fn extract_sources<R: Read>(reader: R) -> Result<HashSet<Opml>, reader::Error> {
let mut list = HashSet::new();
let parser = reader::EventReader::new(reader);
parser
.into_iter()
.map(|e| match e {
Ok(reader::XmlEvent::StartElement {
name, attributes, ..
}) => {
if name.local_name == "outline" {
let mut title = String::new();
let mut url = String::new();
let mut description = String::new();
attributes.into_iter().for_each(|attribute| {
match attribute.name.local_name.as_str() {
"title" => title = attribute.value,
"xmlUrl" => url = attribute.value,
"description" => description = attribute.value,
_ => {}
}
});
let feed = Opml {
title,
description,
url,
};
list.insert(feed);
}
Ok(())
}
Err(err) => Err(err),
_ => Ok(()),
})
.collect::<Result<Vec<_>, reader::Error>>()?;
Ok(list)
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Local;
#[test]
fn test_extract() {
let int_title = String::from("Intercepted with Jeremy Scahill");
let int_url = String::from("https://feeds.feedburner.com/InterceptedWithJeremyScahill");
let int_desc =
String::from(
"The people behind The Intercepts fearless reporting and incisive \
commentary—Jeremy Scahill, Glenn Greenwald, Betsy Reed and others—discuss the \
crucial issues of our time: national security, civil liberties, foreign policy, \
and criminal justice. Plus interviews with artists, thinkers, and newsmakers \
who challenge our preconceptions about the world we live in.",
);
let dec_title = String::from("Deconstructed with Mehdi Hasan");
let dec_url = String::from("https://rss.prod.firstlook.media/deconstructed/podcast.rss");
let dec_desc = String::from(
"Journalist Mehdi Hasan is known around the world for his televised takedowns of \
presidents and prime ministers. In this new podcast from The Intercept, Mehdi \
unpacks a game-changing news event of the week while challenging the conventional \
wisdom. As a Brit, a Muslim and an immigrant based in Donald Trump's Washington \
D.C., Mehdi gives a refreshingly provocative perspective on the ups and downs of \
American—and global—politics.",
);
#[cfg_attr(rustfmt, rustfmt_skip)]
let sample1 = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?> \
<opml version=\"2.0\"> \
<head> \
<title>Test OPML File</title> \
<dateCreated>{}</dateCreated> \
<docs>http://www.opml.org/spec2</docs> \
</head> \
<body> \
<outline type=\"rss\" title=\"{}\" description=\"{}\" xmlUrl=\"{}\"/> \
<outline type=\"rss\" title=\"{}\" description=\"{}\" xmlUrl=\"{}\"/> \
</body> \
</opml>",
Local::now().format("%a, %d %b %Y %T %Z"),
int_title,
int_desc,
int_url,
dec_title,
dec_desc,
dec_url,
);
let map = hashset![
Opml {
title: int_title,
description: int_desc,
url: int_url
},
Opml {
title: dec_title,
description: dec_desc,
url: dec_url
},
];
assert_eq!(extract_sources(sample1.as_bytes()).unwrap(), map);
}
}
@@ -50,24 +50,22 @@ pub fn pipeline<'a, S>(
sources: S,
ignore_etags: bool,
client: &HttpsClient,
) -> Box<Future<Item = Vec<()>, Error = DataError> + 'a>
) -> impl Future<Item = Vec<()>, Error = DataError> + 'a
where
S: Stream<Item = Source, Error = DataError> + 'a,
{
let pipeline = sources
sources
.and_then(clone!(client => move |s| s.into_feed(client.clone(), ignore_etags)))
.and_then(|feed| rayon::scope(|s| s.spawn_future(feed.index())))
// the stream will stop at the first error so
// we ensure that everything will succeded regardless.
.map_err(|err| error!("Error: {}", err))
.then(|_| ok::<(), DataError>(()))
.collect();
Box::new(pipeline)
.collect()
}
/// Creates a tokio `reactor::Core`, and a `hyper::Client` and
/// runs the pipeline.
/// runs the pipeline to completion. The `reactor::Core` is dropped afterwards.
pub fn run<S>(sources: S, ignore_etags: bool) -> Result<(), DataError>
where
S: IntoIterator<Item = Source>,
@@ -105,7 +103,7 @@ mod tests {
/// Insert feeds and update/index them.
fn test_pipeline() {
truncate_db().unwrap();
let bad_url = "https://gitlab.gnome.org/World/hammond.atom";
let bad_url = "https://gitlab.gnome.org/World/podcasts.atom";
// if a stream returns error/None it stops
// bad we want to parse all feeds regardless if one fails
Source::from_url(bad_url).unwrap();
+29
View File
@@ -0,0 +1,29 @@
diff --git a/podcasts-data/src/schema.rs b/podcasts-data/src/schema.rs
index 03cbed0..88f1622 100644
--- a/podcasts-data/src/schema.rs
+++ b/podcasts-data/src/schema.rs
@@ -1,8 +1,11 @@
+#![allow(warnings)]
+
table! {
episodes (title, show_id) {
+ rowid -> Integer,
title -> Text,
uri -> Nullable<Text>,
local_uri -> Nullable<Text>,
description -> Nullable<Text>,
epoch -> Integer,
length -> Nullable<Integer>,
@@ -30,11 +33,7 @@ table! {
uri -> Text,
last_modified -> Nullable<Text>,
http_etag -> Nullable<Text>,
}
}
-allow_tables_to_appear_in_same_query!(
- episodes,
- shows,
- source,
-);
+allow_tables_to_appear_in_same_query!(episodes, shows, source);
@@ -1,5 +1,7 @@
#![allow(warnings)]
table! {
episode (title, podcast_id) {
episodes (title, show_id) {
rowid -> Integer,
title -> Text,
uri -> Nullable<Text>,
@@ -10,22 +12,17 @@ table! {
duration -> Nullable<Integer>,
guid -> Nullable<Text>,
played -> Nullable<Integer>,
favorite -> Bool,
archive -> Bool,
podcast_id -> Integer,
show_id -> Integer,
}
}
table! {
podcast (id) {
shows (id) {
id -> Integer,
title -> Text,
link -> Text,
description -> Text,
image_uri -> Nullable<Text>,
favorite -> Bool,
archive -> Bool,
always_dl -> Bool,
source_id -> Integer,
}
}
@@ -39,4 +36,4 @@ table! {
}
}
allow_tables_to_appear_in_same_query!(episode, podcast, source,);
allow_tables_to_appear_in_same_query!(episodes, shows, source);
@@ -7,7 +7,7 @@ use url::{Position, Url};
use dbqueries;
use errors::DataError;
use models::{EpisodeCleanerQuery, Podcast, Save};
use models::{EpisodeCleanerModel, Save, Show};
use xdg_dirs::DL_DIR;
use std::fs;
@@ -59,7 +59,7 @@ fn played_cleaner(cleanup_date: DateTime<Utc>) -> Result<(), DataError> {
}
/// Check `ep.local_uri` field and delete the file it points to.
fn delete_local_content(ep: &mut EpisodeCleanerQuery) -> Result<(), DataError> {
fn delete_local_content(ep: &mut EpisodeCleanerModel) -> Result<(), DataError> {
if ep.local_uri().is_some() {
let uri = ep.local_uri().unwrap().to_owned();
if Path::new(&uri).exists() {
@@ -108,9 +108,9 @@ pub fn url_cleaner(s: &str) -> String {
}
}
/// Returns the URI of a Podcast Downloads given it's title.
/// Returns the URI of a Show Downloads given it's title.
pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
// It might be better to make it a hash of the title or the podcast rowid
// It might be better to make it a hash of the title or the Show rowid
let download_fold = format!("{}/{}", DL_DIR.to_str().unwrap(), pd_title);
// Create the folder
@@ -123,7 +123,7 @@ pub fn get_download_folder(pd_title: &str) -> Result<String, DataError> {
/// Removes all the entries associated with the given show from the database,
/// and deletes all of the downloaded content.
// TODO: Write Tests
pub fn delete_show(pd: &Podcast) -> Result<(), DataError> {
pub fn delete_show(pd: &Show) -> Result<(), DataError> {
dbqueries::remove_feed(pd)?;
info!("{} was removed succesfully.", pd.title());
@@ -174,7 +174,7 @@ mod tests {
// Clean the db
truncate_db().unwrap();
// Setup tmp file stuff
let tmp_dir = TempDir::new("hammond_test").unwrap();
let tmp_dir = TempDir::new("podcasts_test").unwrap();
let valid_path = tmp_dir.path().join("virtual_dl.mp3");
let bad_path = tmp_dir.path().join("invalid_thing.mp3");
let mut tmp_file = File::create(&valid_path).unwrap();
@@ -183,7 +183,7 @@ mod tests {
// Setup episodes
let n1 = NewEpisodeBuilder::default()
.title("foo_bar".to_string())
.podcast_id(0)
.show_id(0)
.build()
.unwrap()
.to_episode()
@@ -191,14 +191,14 @@ mod tests {
let n2 = NewEpisodeBuilder::default()
.title("bar_baz".to_string())
.podcast_id(1)
.show_id(1)
.build()
.unwrap()
.to_episode()
.unwrap();
let mut ep1 = dbqueries::get_episode_from_pk(n1.title(), n1.podcast_id()).unwrap();
let mut ep2 = dbqueries::get_episode_from_pk(n2.title(), n2.podcast_id()).unwrap();
let mut ep1 = dbqueries::get_episode_from_pk(n1.title(), n1.show_id()).unwrap();
let mut ep2 = dbqueries::get_episode_from_pk(n2.title(), n2.show_id()).unwrap();
ep1.set_local_uri(Some(valid_path.to_str().unwrap()));
ep2.set_local_uri(Some(bad_path.to_str().unwrap()));
@@ -230,7 +230,7 @@ mod tests {
#[test]
fn test_download_cleaner() {
let _tmp_dir = helper_db();
let mut episode: EpisodeCleanerQuery =
let mut episode: EpisodeCleanerModel =
dbqueries::get_episode_from_pk("foo_bar", 0).unwrap().into();
let valid_path = episode.local_uri().unwrap().to_owned();
@@ -1,22 +1,22 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
name = "hammond-downloader"
name = "podcasts-downloader"
version = "0.1.0"
workspace = "../"
[dependencies]
error-chain = "0.11.0"
hyper = "0.11.25"
log = "0.4.1"
mime_guess = "1.8.4"
reqwest = "0.8.5"
error-chain = "0.12.0"
hyper = "0.11.27"
log = "0.4.3"
mime_guess = "1.8.6"
reqwest = "0.8.6"
tempdir = "0.3.7"
glob = "0.2.11"
failure = "0.1.1"
failure_derive = "0.1.1"
[dependencies.hammond-data]
path = "../hammond-data"
[dependencies.podcasts-data]
path = "../podcasts-data"
[dev-dependencies]
pretty_assertions = "0.5.1"
@@ -11,8 +11,8 @@ use std::io::{BufWriter, Read, Write};
use std::path::Path;
use std::sync::{Arc, Mutex};
use hammond_data::xdg_dirs::HAMMOND_CACHE;
use hammond_data::{EpisodeWidgetQuery, PodcastCoverQuery, Save};
use podcasts_data::xdg_dirs::PODCASTS_CACHE;
use podcasts_data::{EpisodeWidgetModel, Save, ShowCoverModel};
// use failure::Error;
use errors::DownloadError;
@@ -78,7 +78,7 @@ fn download_into(
// Construct a temp file to save desired content.
// It has to be a `new_in` instead of new cause rename can't move cross
// filesystems.
let tempdir = TempDir::new_in(HAMMOND_CACHE.to_str().unwrap(), "temp_download")?;
let tempdir = TempDir::new_in(PODCASTS_CACHE.to_str().unwrap(), "temp_download")?;
let out_file = format!("{}/temp.part", tempdir.path().to_str().unwrap(),);
ct_len.map(|x| {
@@ -127,7 +127,7 @@ fn save_io(
info!("Downloading into: {}", file);
let chunk_size = match content_lenght {
Some(x) => x as usize / 99,
None => 1024 as usize, // default chunk size
None => 1024, // default chunk size
};
let mut writer = BufWriter::new(File::create(&file)?);
@@ -161,7 +161,7 @@ fn save_io(
// TODO: Refactor
pub fn get_episode(
ep: &mut EpisodeWidgetQuery,
ep: &mut EpisodeWidgetModel,
download_folder: &str,
progress: Option<Arc<Mutex<DownloadProgress>>>,
) -> Result<(), DownloadError> {
@@ -196,8 +196,9 @@ pub fn get_episode(
Ok(())
}
pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
let url = pd.image_uri()
pub fn cache_image(pd: &ShowCoverModel) -> Result<String, DownloadError> {
let url = pd
.image_uri()
.ok_or_else(|| DownloadError::NoImageLocation)?
.to_owned();
@@ -205,7 +206,7 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
return Err(DownloadError::NoImageLocation);
}
let cache_path = HAMMOND_CACHE
let cache_path = PODCASTS_CACHE
.to_str()
.ok_or_else(|| DownloadError::InvalidCacheLocation)?;
let cache_download_fold = format!("{}{}", cache_path, pd.title().to_owned());
@@ -215,7 +216,8 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
// For some reason there is no .first() method so nth(0) is used
let path = foo.nth(0).and_then(|x| x.ok());
if let Some(p) = path {
return Ok(p.to_str()
return Ok(p
.to_str()
.ok_or_else(|| DownloadError::InvalidCachedImageLocation)?
.into());
}
@@ -234,14 +236,14 @@ pub fn cache_image(pd: &PodcastCoverQuery) -> Result<String, DownloadError> {
#[cfg(test)]
mod tests {
use super::*;
use hammond_data::dbqueries;
use hammond_data::pipeline;
use hammond_data::Source;
use podcasts_data::dbqueries;
use podcasts_data::pipeline;
use podcasts_data::Source;
use std::fs;
#[test]
// This test inserts an rss feed to your `XDG_DATA/hammond/hammond.db` so we make it explicit
// This test inserts an rss feed to your `XDG_DATA/podcasts/podcasts.db` so we make it explicit
// to run it.
#[ignore]
fn test_cache_image() {
@@ -259,7 +261,7 @@ mod tests {
let img_path = cache_image(&pd);
let foo_ = format!(
"{}{}/cover.jpeg",
HAMMOND_CACHE.to_str().unwrap(),
PODCASTS_CACHE.to_str().unwrap(),
pd.title()
);
assert_eq!(img_path.unwrap(), foo_);
@@ -1,4 +1,4 @@
use hammond_data::errors::DataError;
use podcasts_data::errors::DataError;
use reqwest;
use std::io;
+54
View File
@@ -0,0 +1,54 @@
#![recursion_limit = "1024"]
#![allow(unknown_lints)]
#![cfg_attr(
feature = "cargo-clippy",
allow(blacklisted_name, option_map_unit_fn)
)]
// Enable lint group collections
#![warn(
nonstandard_style,
edition_2018,
rust_2018_idioms,
bad_style,
unused
)]
// standalone lints
#![warn(
const_err,
improper_ctypes,
non_shorthand_field_patterns,
no_mangle_generic_items,
overflowing_literals,
plugin_as_library,
private_no_mangle_fns,
private_no_mangle_statics,
unconditional_recursion,
unions_with_drop_fields,
while_true,
missing_debug_implementations,
trivial_casts,
trivial_numeric_casts,
elided_lifetime_in_paths,
missing_copy_implementations
)]
#![deny(warnings)]
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate log;
#[cfg(test)]
#[macro_use]
extern crate pretty_assertions;
extern crate glob;
extern crate hyper;
extern crate mime_guess;
extern crate podcasts_data;
extern crate reqwest;
extern crate tempdir;
pub mod downloader;
pub mod errors;
@@ -1,45 +1,46 @@
[package]
authors = ["Jordan Petridis <jordanpetridis@protonmail.com>"]
build = "build.rs"
name = "hammond-gtk"
name = "podcasts-gtk"
version = "0.1.0"
workspace = "../"
[dependencies]
chrono = "0.4.2"
crossbeam-channel = "0.1.2"
chrono = "0.4.4"
crossbeam-channel = "0.2.3"
gdk = "0.8.0"
gdk-pixbuf = "0.4.0"
glib = "0.5.0"
gstreamer = "0.11.4"
gstreamer-player = "0.11.3"
humansize = "1.1.0"
lazy_static = "1.0.0"
log = "0.4.1"
lazy_static = "1.0.2"
log = "0.4.3"
loggerv = "0.7.1"
open = "1.2.1"
rayon = "1.0.1"
rayon = "1.0.2"
send-cell = "0.1.3"
url = "1.7.0"
url = "1.7.1"
failure = "0.1.1"
failure_derive = "0.1.1"
take_mut = "0.2.2"
regex = "0.2.10"
reqwest = "0.8.5"
serde_json = "1.0.15"
html2pango = { git = "https://gitlab.gnome.org/World/html2pango" }
regex = "1.0.2"
reqwest = "0.8.6"
serde_json = "1.0.24"
html2text = "0.1.8"
[dependencies.gtk]
features = ["v3_22"]
version = "0.4.0"
version = "0.4.1"
[dependencies.gio]
features = ["v2_50"]
version = "0.4.0"
version = "0.4.1"
[dependencies.hammond-data]
path = "../hammond-data"
[dependencies.podcasts-data]
path = "../podcasts-data"
[dependencies.hammond-downloader]
path = "../hammond-downloader"
[dependencies.podcasts-downloader]
path = "../podcasts-downloader"
[dev-dependencies]
pretty_assertions = "0.5.1"
@@ -1,6 +1,10 @@
use std::process::Command;
fn main() {
// Rerun the build script when files in the resources folder are changed.
println!("cargo:rerun-if-changed=resources");
println!("cargo:rerun-if-changed=resources/*");
Command::new("glib-compile-resources")
.args(&["--generate", "resources.xml"])
.current_dir("resources")
+150
View File
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
This file is part of Hammond.
Hammond 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.
Hammond 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 Hammond. If not, see <http://www.gnu.org/licenses/>.
Authors:
Jordan Petridis
Tobias Bernard
-->
<interface>
<requires lib="gtk+" version="3.20"/>
<!-- interface-license-type gplv3 -->
<!-- interface-name Hammond -->
<!-- interface-description A podcast client for the GNOME Desktop -->
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="empty_view">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="pixel_size">256</property>
<property name="icon_name">org.gnome.Podcasts-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="label" translatable="yes">Get some shows</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.4399999999999999"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">6</property>
<property name="column_spacing">12</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Add new shows via feed URL</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">open-menu-symbolic</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Import shows from another device</property>
<style>
<class name="dim-label"/>
</style>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</interface>
@@ -35,21 +35,19 @@ Tobias Bernard
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<object class="GtkBox" id="info_container">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkLabel" id="title_label">
<property name="visible">True</property>
@@ -64,14 +62,7 @@ Tobias Bernard
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
@@ -184,7 +175,7 @@ Tobias Bernard
<object class="GtkLabel" id="total_size">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="label" translatable="yes">XX MB</property>
<property name="label" translatable="yes">Calculating episode size...</property>
<property name="single_line_mode">True</property>
<property name="track_visited_links">False</property>
<style>
@@ -213,16 +204,16 @@ Tobias Bernard
</packing>
</child>
<child>
<object class="GtkBox">
<object class="GtkButtonBox" id="button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">center</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label" translatable="yes">Cancel</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="no_show_all">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
</object>
<packing>
@@ -237,8 +228,8 @@ Tobias Bernard
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Download this episode</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="always_show_image">True</property>
<child>
@@ -252,8 +243,8 @@ Tobias Bernard
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">2</property>
<property name="position">1</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
@@ -262,6 +253,7 @@ Tobias Bernard
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="tooltip_text" translatable="yes">Play this episode</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
@@ -274,15 +266,14 @@ Tobias Bernard
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">3</property>
<property name="position">2</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">6</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">_Check for new episodes</attribute>
<attribute name="action">win.refresh</attribute>
<attribute name="accel">&lt;primary&gt;r</attribute>
</item>
<item>
<attribute name="label" translatable="yes">_Import Shows</attribute>
<attribute name="action">win.import</attribute>
</item>
<!-- <item> -->
<!-- <attribute name="label" translatable="yes">_Export Shows</attribute> -->
<!-- <attribute name="action">win.export</attribute> -->
<!-- </item> -->
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Preferences</attribute>
<attribute name="action">win.preferences</attribute>
<attribute name="accel">&lt;primary&gt;e</attribute>
</item>
</section>
<section>
<item>
<attribute name="label" translatable="yes">_Keyboard Shortcuts</attribute>
<attribute name="action">win.show-help-overlay</attribute>
</item>
<!-- <item> -->
<!-- <attribute name="label" translatable="yes">_Help</attribute> -->
<!-- <attribute name="action">win.help</attribute> -->
<!-- </item> -->
<item>
<attribute name="label" translatable="yes">_About</attribute>
<attribute name="action">win.about</attribute>
</item>
</section>
</menu>
</interface>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
@@ -151,9 +151,10 @@ Tobias Bernard
<object class="GtkMenuButton" id="add_toggle">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Add a new feed</property>
<property name="valign">center</property>
<property name="popover">add_popover</property>
<child>
<object class="GtkImage" id="add-button-image2">
<property name="visible">True</property>
@@ -168,7 +169,7 @@ Tobias Bernard
</object>
</child>
<child>
<object class="GtkButton" id="back_button">
<object class="GtkButton" id="back">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="no_show_all">True</property>
@@ -257,12 +258,11 @@ Tobias Bernard
</object>
</child>
<child>
<object class="GtkMenuButton" id="menu_toggle">
<object class="GtkMenuButton" id="hamburger">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="valign">center</property>
<property name="popover">menu_popover</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
@@ -280,99 +280,28 @@ Tobias Bernard
<property name="position">2</property>
</packing>
</child>
</object>
<object class="GtkPopover" id="menu_popover">
<property name="can_focus">False</property>
<property name="relative_to">menu_toggle</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<object class="GtkMenuButton" id="secondary_menu">
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Preferences</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<property name="no_show_all">True</property>
<property name="valign">center</property>
<child>
<object class="GtkModelButton" id="update_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Check for new episodes</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">view-more-symbolic</property>
<property name="icon_size">1</property>
</object>
</child>
<style>
<class name="image-button"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="about_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">About</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">6</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Help</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">7</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="text" translatable="yes">Keyboard Shortcuts</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">8</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">True</property>
<child>
<object class="GtkShortcutsSection">
<property name="section-name">shortcuts</property>
<property name="max-height">12</property>
<property name="visible">True</property>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">General</property>
<property name="visible">True</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">True</property>
<property name="accelerator">&lt;primary&gt;r</property>
<property name="title" translatable="yes" context="shortcut window">Check for new episodes</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">True</property>
<property name="accelerator">&lt;primary&gt;e</property>
<property name="title" translatable="yes" context="shortcut window">Preferences</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">True</property>
<property name="accelerator">&lt;primary&gt;q</property>
<property name="title" translatable="yes" context="shortcut window">Quit the application</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
@@ -36,12 +36,11 @@ Tobias Bernard
<property name="valign">center</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_left">8</property>
<property name="pixel_size">64</property>
<property name="icon_name">image-x-generic-symbolic</property>
</object>
@@ -32,8 +32,8 @@ Tobias Bernard
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="name">container</property>
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="width_request">290</property>
<property name="height_request">420</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@@ -0,0 +1,371 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkImage" id="fast_forward">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">skip-forward-symbolic</property>
</object>
<object class="GtkImage" id="pause_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">1</property>
<property name="icon_name">media-playback-pause-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="play_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">1</property>
<property name="icon_name">media-playback-start-symbolic</property>
<property name="icon_size">1</property>
</object>
<object class="GtkImage" id="rewind">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">skip-back-symbolic</property>
</object>
<object class="GtkActionBar" id="action_bar">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="valign">center</property>
<child>
<object class="GtkBox" id="buttons">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<child>
<object class="GtkButton" id="rewind_button">
<property name="width_request">42</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Rewind 10 seconds</property>
<property name="image">rewind</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="play_button">
<property name="width_request">60</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Play</property>
<property name="image">play_image</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="pause_button">
<property name="width_request">60</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Pause</property>
<property name="image">pause_image</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ff_button">
<property name="width_request">42</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Fast Forward 10 seconds</property>
<property name="image">fast_forward</property>
<property name="always_show_image">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">3</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox" id="info">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="show_cover">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="valign">center</property>
<property name="pixel_size">34</property>
<property name="icon_name">image-x-generic-symbolic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="show_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Show Title</property>
<property name="wrap">True</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">20</property>
<style>
<class name="player-show-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="episode_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="label" translatable="yes">Episode Title</property>
<property name="wrap">True</property>
<property name="ellipsize">end</property>
<property name="max_width_chars">20</property>
<style>
<class name="player-episode-label"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkScale" id="seek">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="round_digits">1</property>
<property name="draw_value">False</property>
</object>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkBox" id="timer">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="progress_time_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="separator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label">/</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="total_duration_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="label">0:00</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="rate_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Change the Playback speed</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="margin_left">6</property>
<property name="direction">up</property>
<property name="popover">rate_popover</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel" id="rate_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">1.00x</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">go-down-symbolic</property>
<property name="icon_size">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="position">4</property>
</packing>
</child>
</object>
<object class="GtkPopover" id="rate_popover">
<property name="can_focus">False</property>
<property name="relative_to">rate_button</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">6</property>
<property name="margin_right">6</property>
<property name="margin_top">6</property>
<property name="margin_bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">3</property>
<child>
<object class="GtkRadioButton" id="rate_1_50">
<property name="label" translatable="yes">1.50x</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">1.5 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="rate_1_25">
<property name="label" translatable="yes">1.25x</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">1.25 speed rate</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="draw_indicator">True</property>
<property name="group">normal_rate</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkRadioButton" id="normal_rate">
<property name="label" translatable="yes">1.00x</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="tooltip_text" translatable="yes">Normal speed</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
+282
View File
@@ -0,0 +1,282 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="cleanup_value_adj">
<property name="lower">1</property>
<property name="upper">100</property>
<property name="value">1</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkWindow" id="prefs">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Preferences</property>
<property name="type_hint">dialog</property>
<child>
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="min_content_width">480</property>
<property name="min_content_height">200</property>
<property name="propagate_natural_width">True</property>
<property name="propagate_natural_height">True</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="shadow_type">none</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_top">10</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Appearance</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="width_request">450</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
<child>
<object class="GtkListBoxRow">
<property name="height_request">50</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Dark Theme</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkSwitch" id="dark_toggle">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="margin_top">20</property>
<property name="margin_bottom">5</property>
<property name="label" translatable="yes">Delete played episodes</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkFrame">
<property name="width_request">300</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label_xalign">0</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkAlignment">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkListBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="selection_mode">none</property>
<property name="activate_on_single_click">False</property>
<child>
<object class="GtkListBoxRow">
<property name="width_request">100</property>
<property name="height_request">50</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="activatable">False</property>
<property name="selectable">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">5</property>
<property name="margin_right">5</property>
<property name="margin_top">5</property>
<property name="margin_bottom">5</property>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">After</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkSpinButton" id="cleanup_value">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="input_purpose">digits</property>
<property name="adjustment">cleanup_value_adj</property>
<property name="climb_rate">1</property>
<property name="value">1</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="cleanup_type">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<style>
<class name="linked"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="pack_type">end</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</child>
<child type="label_item">
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title">Preferences</property>
<property name="show_close_button">True</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<object class="GtkAdjustment" id="refresh_value_adj">
<property name="lower">1</property>
<property name="upper">100</property>
<property name="value">1</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
</interface>
@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<interface>
<!-- interface-requires gtk+ 3.0 -->
<menu id="menu">
<section>
<item>
<attribute name="label" translatable="yes">_Mark all episodes as played</attribute>
<!-- <attribute name="action">win.refresh</attribute> -->
</item>
<item>
<attribute name="label" translatable="yes">_Website</attribute>
<!-- <attribute name="action">win.import</attribute> -->
</item>
<item>
<attribute name="label" translatable="yes">_Unsubscribe</attribute>
<!-- <attribute name="action">win.export</attribute> -->
</item>
</section>
</menu>
</interface>
+76
View File
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkPopoverMenu" id="menu">
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="border_width">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkModelButton" id="website">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Open Website</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="played">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Mark all as played</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkSeparator">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="unsub">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="text" translatable="yes">Unsubscribe</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="submenu">main</property>
<property name="position">1</property>
</packing>
</child>
</object>
</interface>
@@ -31,8 +31,8 @@ Tobias Bernard
<!-- interface-copyright 2017 - 2018 -->
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="container">
<property name="width_request">400</property>
<property name="height_request">600</property>
<property name="width_request">290</property>
<property name="height_request">420</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@@ -79,7 +79,7 @@ Tobias Bernard
<property name="margin_bottom">32</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="spacing">24</property>
<property name="spacing">6</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
@@ -133,79 +133,11 @@ Sorry, we could not find a description for this Show.</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="spacing">6</property>
<child>
<object class="GtkMenuButton" id="settings_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="icon_name">emblem-system-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="link_button">
<property name="label" translatable="yes">Website</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="unsub_button">
<property name="label" translatable="yes">Unsubscribe</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">center</property>
<property name="valign">center</property>
<style>
<class name="destructive-action"/>
</style>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="padding">5</property>
<property name="pack_type">end</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="padding">6</property>
<property name="position">1</property>
</packing>
</child>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.21.0
<!-- Generated with glade 3.22.0
Copyright (C) 2017 - 2018
@@ -32,6 +32,8 @@ Tobias Bernard
<!-- interface-authors Jordan Petridis\nTobias Bernard -->
<object class="GtkBox" id="fb_parent">
<property name="name">fb_parent</property>
<property name="width_request">290</property>
<property name="height_request">420</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
@@ -9,3 +9,12 @@ row:last-child {
list, border {
border-radius: 4px;
}
.player-episode-label {
font-weight: bold;
font-size: smaller;
}
.player-show-label {
font-size: smaller;
}
@@ -0,0 +1,617 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
inkscape:export-ydpi="96"
inkscape:export-xdpi="96"
inkscape:export-filename="Template.png"
width="128"
height="128"
id="svg11300"
sodipodi:version="0.32"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="org.gnome.Podcasts.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
version="1.0"
style="display:inline;enable-background:new"
viewBox="0 0 128 128">
<title
id="title4162">Adwaita Icon Template</title>
<defs
id="defs3">
<linearGradient
inkscape:collect="always"
id="linearGradient1654">
<stop
style="stop-color:#c65f00;stop-opacity:1"
offset="0"
id="stop1642" />
<stop
id="stop1644"
offset="0.03432177"
style="stop-color:#ff7b00;stop-opacity:1" />
<stop
style="stop-color:#c65f00;stop-opacity:1"
offset="0.07062413"
id="stop1646" />
<stop
id="stop1648"
offset="0.92767543"
style="stop-color:#c65f00;stop-opacity:1" />
<stop
style="stop-color:#ff7b00;stop-opacity:1"
offset="0.96428573"
id="stop1650" />
<stop
style="stop-color:#c65f00;stop-opacity:1"
offset="1"
id="stop1652" />
</linearGradient>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1017">
<rect
ry="96.000015"
rx="96.000015"
y="-163.5"
x="159.99997"
height="367.5"
width="192.00003"
id="rect1020"
style="opacity:1;fill:#f1bd10;fill-opacity:1;stroke:none;stroke-width:0.99999988;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:4.00000002, 2.00000002;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath1090">
<rect
ry="31.999998"
rx="32"
y="-115.99999"
x="32.5"
height="320"
width="447"
id="rect1092"
style="opacity:1;fill:#fb8c14;fill-opacity:1;stroke:none;stroke-width:0.99999994;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:3.9999998, 1.9999999;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath945">
<rect
ry="31.999998"
rx="32"
y="-115.99999"
x="32.5"
height="320"
width="447"
id="rect943"
style="opacity:1;fill:#fb8c14;fill-opacity:1;stroke:none;stroke-width:0.99999994;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:3.9999998, 1.9999999;stroke-dashoffset:0;stroke-opacity:1" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath904">
<rect
y="4"
x="208"
height="48"
width="96"
id="rect906"
style="opacity:1;fill:#949390;fill-opacity:1;stroke:none;stroke-width:15.99999905;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:11.19999981;stroke-opacity:1" />
</clipPath>
<linearGradient
id="linearGradient1093"
inkscape:collect="always">
<stop
id="stop1089"
offset="0"
style="stop-color:#4fd2fd;stop-opacity:1" />
<stop
id="stop1091"
offset="1"
style="stop-color:#0aa8dc;stop-opacity:1" />
</linearGradient>
<clipPath
id="clipPath1567"
clipPathUnits="userSpaceOnUse">
<circle
transform="rotate(-135)"
r="24"
cy="-134.35028"
cx="-196.57568"
id="circle1569"
style="display:inline;opacity:1;vector-effect:none;fill:url(#radialGradient1571);fill-opacity:1;stroke:none;stroke-width:0.24999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</clipPath>
<radialGradient
r="24"
fy="222.27728"
fx="40.287689"
cy="222.27728"
cx="40.287689"
gradientTransform="matrix(1.3314563,0,0,1.3314563,-253.92929,-440.0255)"
gradientUnits="userSpaceOnUse"
id="radialGradient1571"
xlink:href="#linearGradient1093"
inkscape:collect="always" />
<clipPath
id="clipPath1584"
clipPathUnits="userSpaceOnUse">
<circle
transform="rotate(-45)"
r="24"
cy="195.16147"
cx="-132.93608"
id="circle1586"
style="display:inline;opacity:1;vector-effect:none;fill:url(#radialGradient1588);fill-opacity:1;stroke:none;stroke-width:0.24999999;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
</clipPath>
<radialGradient
r="24"
fy="222.27728"
fx="40.287689"
cy="222.27728"
cx="40.287689"
gradientTransform="matrix(1.3314563,0,0,1.3314563,-190.28968,-110.51374)"
gradientUnits="userSpaceOnUse"
id="radialGradient1588"
xlink:href="#linearGradient1093"
inkscape:collect="always" />
<clipPath
id="clipPath1757"
clipPathUnits="userSpaceOnUse">
<circle
r="120.00001"
cy="-200"
cx="36"
id="circle1759"
style="display:inline;opacity:1;fill:#424048;fill-opacity:1;stroke:none;stroke-width:0.99999994;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:3.99999981, 1.99999992;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
transform="rotate(90)" />
</clipPath>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1654"
id="linearGradient1640"
x1="8"
y1="264"
x2="120"
y2="264"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
stroke="#ef2929"
fill="#f57900"
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="0.25490196"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.70710678"
inkscape:cx="-263.96829"
inkscape:cy="8.5799908"
inkscape:current-layer="layer9"
showgrid="false"
inkscape:grid-bbox="true"
inkscape:document-units="px"
inkscape:showpageshadow="false"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1920"
inkscape:window-y="27"
width="400px"
height="300px"
inkscape:snap-nodes="true"
inkscape:snap-bbox="false"
objecttolerance="7"
gridtolerance="12"
guidetolerance="13"
inkscape:window-maximized="1"
inkscape:pagecheckerboard="false"
showguides="false"
inkscape:guide-bbox="true"
inkscape:locked="false"
inkscape:measure-start="0,0"
inkscape:measure-end="0,0"
inkscape:object-nodes="true"
inkscape:bbox-nodes="true"
inkscape:snap-global="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
showborder="false"
inkscape:snap-center="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-midpoints="true"
inkscape:snap-smooth-nodes="true">
<inkscape:grid
type="xygrid"
id="grid5883"
spacingx="2"
spacingy="2"
enabled="true"
visible="true"
empspacing="4"
originx="0"
originy="0" />
<sodipodi:guide
position="64,8"
orientation="0,1"
id="guide1073"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="12,39"
orientation="1,0"
id="guide1075"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="72,104"
orientation="0,1"
id="guide1099"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,128"
orientation="0,1"
id="guide993"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="104,64"
orientation="1,0"
id="guide995"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="0,72"
orientation="1,0"
id="guide867"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="120,58"
orientation="1,0"
id="guide869"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="84.25,116"
orientation="0,1"
id="guide871"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<inkscape:grid
type="xygrid"
id="grid873"
spacingx="1"
spacingy="1"
empspacing="8"
color="#000000"
opacity="0.49019608"
empcolor="#000000"
empopacity="0.08627451"
dotted="true" />
<sodipodi:guide
position="24,37"
orientation="1,0"
id="guide877"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="116,58"
orientation="1,0"
id="guide879"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,120"
orientation="0,1"
id="guide881"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="59.75,12"
orientation="0,1"
id="guide883"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="8,39"
orientation="1,0"
id="guide885"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="128,56"
orientation="1,0"
id="guide887"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,0"
orientation="0,1"
id="guide897"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="56,24"
orientation="0,1"
id="guide899"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="256,256"
orientation="-0.70710678,0.70710678"
id="guide950"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="64,64"
orientation="0.70710678,0.70710678"
id="guide952"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<metadata
id="metadata4">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>GNOME Design Team</dc:title>
</cc:Agent>
</dc:creator>
<dc:source />
<cc:license
rdf:resource="http://creativecommons.org/licenses/by-sa/4.0/" />
<dc:title>Adwaita Icon Template</dc:title>
<dc:subject>
<rdf:Bag />
</dc:subject>
<dc:date />
<dc:rights>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:rights>
<dc:publisher>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:publisher>
<dc:identifier />
<dc:relation />
<dc:language />
<dc:coverage />
<dc:description />
<dc:contributor>
<cc:Agent>
<dc:title />
</cc:Agent>
</dc:contributor>
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/by-sa/4.0/">
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Notice" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#Attribution" />
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks" />
<cc:requires
rdf:resource="http://creativecommons.org/ns#ShareAlike" />
</cc:License>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Icon"
inkscape:groupmode="layer"
style="display:inline"
transform="translate(0,-172)">
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="baseplate"
style="display:none"
sodipodi:insensitive="true">
<text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.08791494px;line-height:125%;font-family:Cantarell;-inkscape-font-specification:'Cantarell, Normal';text-align:start;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33264872;enable-background:new"
x="7.9499588"
y="148.65199"
id="context"
inkscape:label="context"><tspan
sodipodi:role="line"
id="tspan2716"
x="7.9499588"
y="148.65199"
style="stroke-width:0.33264872">apps</tspan></text>
<text
inkscape:label="icon-name"
id="text3021"
y="157.23398"
x="7.7533054"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:6.08791494px;line-height:125%;font-family:Cantarell;-inkscape-font-specification:'Cantarell, Bold';text-align:start;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.33264872;enable-background:new"
xml:space="preserve"><tspan
y="157.23398"
x="7.7533054"
id="tspan3023"
sodipodi:role="line"
style="stroke-width:0.33264872">org.gnome.Podcasts</tspan></text>
<g
style="display:inline;fill:#000000;enable-background:new"
transform="matrix(7.9911709,0,0,8.0036407,-167.7909,-4846.0776)"
id="g12027"
inkscape:export-xdpi="12"
inkscape:export-ydpi="12" />
<rect
style="display:inline;overflow:visible;visibility:visible;fill:#f0f0f0;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;marker:none;enable-background:accumulate"
id="rect13805"
width="128"
height="128"
x="9.2651362e-08"
y="172"
inkscape:label="512x512" />
<g
id="g883"
style="fill:none;fill-opacity:0.25098039;stroke:#a579b3;stroke-opacity:1"
transform="translate(-24,24)" />
<g
id="g900"
style="fill:none;fill-opacity:0.25098039;stroke:#a579b3;stroke-opacity:1"
transform="translate(-24,24)" />
<g
id="g1168"
transform="matrix(0.25,0,0,0.25,6.9488522e-8,225)">
<rect
ry="32"
rx="32"
y="-180"
x="96"
height="448"
width="319.99979"
id="rect1110"
style="opacity:0.1;fill:#2864b0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
<rect
ry="32"
rx="32"
y="-164"
x="48"
height="416"
width="416"
id="rect1110-8"
style="display:inline;opacity:0.1;fill:#2864b0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<rect
ry="32"
rx="32"
y="-116"
x="32"
height="320"
width="448"
id="rect1110-8-9"
style="display:inline;opacity:0.1;fill:#2864b0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal;enable-background:new" />
<circle
cx="256"
cy="44"
r="240"
id="path1142"
style="opacity:0.1;fill:#2864b0;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none;paint-order:normal" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer9"
inkscape:label="hires"
style="display:inline">
<rect
ry="8"
rx="8"
y="252"
x="8"
height="24"
width="112"
id="rect1025"
style="display:inline;opacity:1;fill:url(#linearGradient1640);fill-opacity:1.0;stroke:none;stroke-width:0.24999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.99999995, 0.49999997000000002;stroke-dashoffset:0;stroke-opacity:1;enable-background:new" />
<rect
style="display:inline;opacity:1;fill:#ff9533;fill-opacity:1;stroke:none;stroke-width:0.24999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.99999995, 0.49999997000000002;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
id="rect927-7"
width="112"
height="75.999992"
x="8"
y="196.00002"
rx="8"
ry="8" />
<path
clip-path="none"
style="display:inline;fill:#949390;fill-opacity:1;stroke:#949390;stroke-width:8;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1;enable-background:new"
d="m 35.999995,270 h 56 l -4,-10 h -48 z"
id="path1083-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<rect
style="display:inline;opacity:1;fill:#6b696c;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
id="rect915"
width="64"
height="6.0000076"
x="32"
y="270" />
<path
style="display:inline;enable-background:new;opacity:1;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:0.24999997;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.99999998, 0.49999998000000001;stroke-dashoffset:0;stroke-opacity:1"
d="M 23.999999,204 H 104 l 8,8 v 44.00001 H 15.999999 V 212 Z"
id="path1203-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc" />
<g
id="g1565">
<rect
y="218"
x="16"
height="28"
width="96"
id="rect1050"
style="display:inline;opacity:1;fill:#424048;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;enable-background:new" />
<circle
r="6"
cy="232"
cx="39.999992"
id="path1060-5"
style="display:inline;opacity:1;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:0.24999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.99999996, 0.49999998;stroke-dashoffset:0;stroke-opacity:1;enable-background:new" />
<circle
style="display:inline;opacity:1;fill:#f6f5f4;fill-opacity:1;stroke:none;stroke-width:0.24999999;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:0.99999996, 0.49999998;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
id="circle1062-6"
cx="87.999992"
cy="232"
r="6" />
<rect
y="226"
x="51.999992"
height="12"
width="24"
id="rect1271-3"
style="display:inline;opacity:1;fill:#949390;fill-opacity:1;stroke:none;stroke-width:3.99999976;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:11.19999981;stroke-opacity:1;enable-background:new" />
<path
sodipodi:nodetypes="sssss"
inkscape:connector-curvature="0"
id="ellipse900"
d="M 272,28 C 272,89.855892 221.85589,140 160,140 98.144108,140 48,89.855892 48,28 48,-33.855892 98.144108,-84 160,-84 c 61.85589,0 112,50.144108 112,112 z"
clip-path="url(#clipPath904)"
style="display:inline;opacity:0.5;fill:#424048;fill-opacity:1;stroke:none;stroke-width:0.99999982;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:3.99999983, 1.99999994;stroke-dashoffset:0;stroke-opacity:1;enable-background:new"
transform="matrix(0.25,0,0,0.25,-4.5e-6,225.00001)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
version="1.1"
id="svg7384"
height="16"
sodipodi:docname="org.gnome.Podcasts-symbolic.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview16"
showgrid="false"
inkscape:zoom="14.75"
inkscape:cx="-14.813559"
inkscape:cy="6.9152542"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g876" />
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs7386">
<linearGradient
osb:paint="solid"
id="linearGradient7212">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop7214" />
</linearGradient>
</defs>
<g
transform="translate(-323.0002,125)"
style="display:inline"
id="layer9">
<g
transform="translate(311.00017,-893)"
id="g876"
style="enable-background:new">
<path
style="display:inline;opacity:1;vector-effect:none;fill:#bebebe;fill-opacity:1;stroke:none;stroke-width:2.61861467;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.43700789"
d="M 1 3 C 0.446 3 0 3.446 0 4 L 0 13 C 0 13.554 0.446 14 1 14 L 15 14 C 15.554 14 16 13.554 16 13 L 16 4 C 16 3.446 15.554 3 15 3 L 1 3 z M 1 6 L 15 6 L 15 10 L 1 10 L 1 6 z M 4 7 A 1 1 0 0 0 3 8 A 1 1 0 0 0 4 9 A 1 1 0 0 0 5 8 A 1 1 0 0 0 4 7 z M 6 7 L 6 9 L 10 9 L 10 7 L 6 7 z M 12 7 A 1 1 0 0 0 11 8 A 1 1 0 0 0 12 9 A 1 1 0 0 0 13 8 A 1 1 0 0 0 12 7 z M 5 11 L 11 11 L 12 13 L 4 13 L 5 11 z "
transform="translate(12.00003,768)"
id="rect2029" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+1
View File
@@ -0,0 +1 @@
install_subdir('hicolor', install_dir: icondir)
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
version="1.1"
id="svg7384"
height="16"
sodipodi:docname="skip-back-symbolic.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview16"
showgrid="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-text-baseline="true"
inkscape:snap-page="true"
inkscape:zoom="14.75"
inkscape:cx="-5.302011"
inkscape:cy="11.053898"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="g1104">
<inkscape:grid
type="xygrid"
id="grid837" />
</sodipodi:namedview>
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs7386">
<linearGradient
osb:paint="solid"
id="linearGradient7212">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop7214" />
</linearGradient>
</defs>
<g
id="g1104"
transform="translate(-285.00026,-511.51971)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
d="m 293.25027,512.51971 c -0.13304,0.006 -0.26338,0.0444 -0.375,0.11719 l -3.75,2.25 c -0.2118,0.13478 -0.32813,0.3838 -0.32813,0.63281 0,0.24901 0.11633,0.49803 0.32813,0.63281 l 3.75,2.25 c 0.11162,0.0728 0.24196,0.11119 0.375,0.11719 h 0.11719 0.6328 v -6 h -0.6328 z"
id="path1096-6-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccscccccccc" />
<path
inkscape:connector-curvature="0"
id="path1098-7"
d="m 294.00002,514.51953 c 2.61534,0 4.93816,1.703 5.72461,4.19727 0.78645,2.49427 -0.14084,5.22062 -2.2832,6.7207 a 1.0000999,1.0000999 0 1 1 -1.14649,-1.63867 c 1.43403,-1.00411 2.04791,-2.8109 1.52149,-4.48047 -0.52643,-1.66957 -2.06577,-2.79883 -3.81641,-2.79883 a 1.0000999,1.0000999 0 1 1 0,-2 z"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
<path
d="m 285.51611,526.51971 h 3.72666 v -1.28266 h -1.066 v -4.732 h -0.89266 l -2.07134,1.196 0.56334,1.07466 0.90133,-0.468 v 2.92934 h -1.16133 z"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:8.66666698px;line-height:1.25;font-family:Cantarell;-inkscape-font-specification:'Cantarell Ultra-Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:0.99999976"
id="path844"
inkscape:connector-curvature="0" />
<path
d="m 292.11307,526.60638 c 1.52533,0 2.55666,-1.248 2.55666,-3.12867 0,-1.83733 -1.014,-3.05933 -2.51333,-3.05933 -1.534,0 -2.56533,1.248 -2.56533,3.12 0,1.83733 1.014,3.068 2.522,3.068 z m 0.0347,-1.28267 c -0.61533,0 -1.014,-0.73666 -1.014,-1.83733 0,-1.07467 0.39867,-1.78533 0.988,-1.78533 0.598,0 1.00534,0.73666 1.00534,1.846 0,1.066 -0.39867,1.77666 -0.97934,1.77666 z"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:8.66666698px;line-height:1.25;font-family:Cantarell;-inkscape-font-specification:'Cantarell Ultra-Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:0.99999976"
id="path846"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16"
version="1.1"
id="svg7384"
height="16"
sodipodi:docname="skip-forward-symbolic.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview16"
showgrid="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-object-midpoints="true"
inkscape:snap-center="true"
inkscape:snap-text-baseline="true"
inkscape:snap-page="true"
inkscape:zoom="20.85965"
inkscape:cx="12.060999"
inkscape:cy="12.860546"
inkscape:window-x="1920"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg7384">
<inkscape:grid
type="xygrid"
id="grid835" />
</sodipodi:namedview>
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs7386">
<linearGradient
osb:paint="solid"
id="linearGradient7212">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop7214" />
</linearGradient>
</defs>
<path
inkscape:connector-curvature="0"
id="path849"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:8.66666698px;line-height:1.25;font-family:Cantarell;-inkscape-font-specification:'Cantarell Ultra-Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:0.99999976"
d="m 6.515284,15.000003 h 3.726667 V 13.717336 H 9.1759508 v -4.732 H 8.2832841 l -2.0713334,1.196 0.5633333,1.074667 0.9013334,-0.468 v 2.929333 H 6.515284 Z" />
<path
inkscape:connector-curvature="0"
id="path851"
style="font-style:normal;font-variant:normal;font-weight:800;font-stretch:normal;font-size:8.66666698px;line-height:1.25;font-family:Cantarell;-inkscape-font-specification:'Cantarell Ultra-Bold';text-align:center;letter-spacing:0px;word-spacing:0px;text-anchor:middle;fill:#2e3436;fill-opacity:1;stroke:none;stroke-width:0.99999976"
d="m 13.112242,15.08667 c 1.525334,0 2.556667,-1.248001 2.556667,-3.128667 0,-1.837334 -1.014,-3.0593337 -2.513333,-3.0593337 -1.534,0 -2.565334,1.2479997 -2.565334,3.1199997 0,1.837334 1.014,3.068001 2.522,3.068001 z m 0.03467,-1.282667 c -0.615333,0 -1.014,-0.736667 -1.014,-1.837334 0,-1.074666 0.398667,-1.785333 0.988,-1.785333 0.598,0 1.005333,0.736667 1.005333,1.846 0,1.066 -0.398666,1.776667 -0.979333,1.776667 z" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:'Bitstream Vera Sans';-inkscape-font-specification:'Bitstream Vera Sans';text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-anchor:start;display:inline;overflow:visible;visibility:visible;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;enable-background:accumulate"
d="m 7.74999,1 c 0.13304,0.006 0.26338,0.0444 0.375,0.11719 l 3.749999,2.2499999 c 0.2118,0.13478 0.32813,0.3838 0.32813,0.63281 0,0.24901 -0.11633,0.49803 -0.32813,0.63281 L 8.12499,6.8828096 c -0.11162,0.0728 -0.24196,0.11119 -0.375,0.11719 H 7.6328 7 V 1 h 0.6328 z"
id="path946"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccscccccccc" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.99999988;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
d="m 7,3 c -2.615338,-10e-8 -4.9362036,1.702994 -5.7226562,4.1972656 -0.78645273,2.4942717 0.1388853,5.2206254 2.28125,6.7207034 A 1.0000999,1.0000999 0 1 0 4.7050781,12.279297 C 3.2710452,11.275191 2.6571722,9.4683988 3.1835938,7.7988281 3.7100153,6.1292575 5.2493627,5 7,5 A 1.0000999,1.0000999 0 1 0 7,3 Z"
id="path948"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

+13
View File
@@ -0,0 +1,13 @@
subdir('icons')
install_data('org.gnome.Podcasts.desktop', install_dir : datadir + '/applications')
install_data('org.gnome.Podcasts.appdata.xml', install_dir : datadir + '/appdata')
install_data('org.gnome.Podcasts.gschema.xml', install_dir: join_paths(datadir, 'glib-2.0', 'schemas'))
configure_file(input: 'org.gnome.Podcasts.service.in',
output: 'org.gnome.Podcasts.service',
configuration: podcasts_conf,
install_dir: join_paths([datadir,'dbus-1/services']))
meson.add_install_script('../../scripts/compile-gschema.py')
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop">
<id>org.gnome.Podcasts</id>
<name>Podcasts</name>
<project_license>GPL-3.0</project_license>
<metadata_license>CC0-1.0</metadata_license>
<developer_name>Jordan Petridis</developer_name>
<summary>Podcast app for GNOME</summary>
<description xml:lang="en">
<p>
Listen to your favorite podcasts, right from your desktop.
</p>
</description>
<url type="homepage">https://gitlab.gnome.org/World/podcasts</url>
<screenshots>
<screenshot>
<image>https://gitlab.gnome.org/World/podcasts/raw/master/screenshots/home_view.png</image>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/podcasts/raw/master/screenshots/shows_view.png</image>
</screenshot>
<screenshot>
<image>https://gitlab.gnome.org/World/podcasts/raw/master/screenshots/show_widget.png</image>
</screenshot>
</screenshots>
<releases>
<release version="0.4.3" date="2018-07-28"/>
</releases>
<url type="homepage">https://gitlab.gnome.org/World/podcasts</url>
<update_contact>jpetridis@gnome.org</update_contact>
<developer_name>Jordan Petridis and others</developer_name>
</component>
@@ -0,0 +1,11 @@
[Desktop Entry]
Name=Podcasts
Comment=Listen to your favorite podcasts, right from your desktop.
Icon=org.gnome.Podcasts
Exec=gnome-podcasts
Terminal=false
Type=Application
StartupNotify=true
Categories=GNOME;GTK;AudioVideo;Audio;
Keywords=Podcast;RSS;
DBusActivatable=true

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