Compare commits

...

176 Commits

Author SHA1 Message Date
Robert McRackan
0a6e55dcb7 * Much faster library scans
* Libraries of unlimited size now supported (prev limit was 10k)
2022-05-26 11:33:05 -04:00
rmcrackan
99b77decff Merge pull request #260 from Mbucari/master
Throttle episode scanning to 10 concurrent scans.
2022-05-26 11:29:18 -04:00
Robert McRackan
9e2ca4e586 update dependencies 2022-05-26 10:45:57 -04:00
Michael Bucari-Tovo
2e8acfdeef Limnit episode concurrency to 5 2022-05-26 08:44:39 -06:00
Michael Bucari-Tovo
630096e06d Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-26 08:43:33 -06:00
Michael Bucari-Tovo
d92d892dc7 logging 2022-05-25 20:45:18 -06:00
Michael Bucari-Tovo
a8f41841bd Throttle episode scanning to 10 concurrent scans. 2022-05-25 20:43:12 -06:00
rmcrackan
76954b5a0a Merge pull request #258 from Mbucari/master
Add support for unlimited library size.
2022-05-25 22:00:22 -04:00
Michael Bucari-Tovo
c57b184a09 Remove test params 2022-05-25 16:51:25 -06:00
Michael Bucari-Tovo
20ca4e0739 Refactor for clarity. 2022-05-25 16:49:22 -06:00
Mbucari
a972ed5e2e Merge branch 'rmcrackan:master' into master 2022-05-25 16:05:31 -06:00
Michael Bucari-Tovo
2b15bc6ebb Count Items as they come in and log total. 2022-05-25 15:11:38 -06:00
Robert McRackan
f7a482659c New feature #241 : Auto download episodes after scanning. Setting is on Import Library tab 2022-05-25 15:21:28 -04:00
Robert McRackan
99527453a7 add TODO 2022-05-25 12:56:34 -04:00
Robert McRackan
3408b4637c search engine cleanup 2022-05-25 12:49:24 -04:00
Robert McRackan
3f2899e97e * New event SearchEngineCommands.SearchEngineUpdated
* Clean up redundant event notifications
2022-05-25 10:09:27 -04:00
Michael Bucari-Tovo
562496cfaa Add more logging 2022-05-24 21:36:56 -06:00
Michael Bucari-Tovo
8283f19d6b Parallelize getChildEpisodesAsync 2022-05-24 21:17:59 -06:00
Michael Bucari-Tovo
242909b542 Don't import empty episode 2022-05-24 18:39:47 -06:00
Michael Bucari-Tovo
a7b83ad5e0 Remove 10,000 book limitation and simplify episode import 2022-05-24 18:27:20 -06:00
Michael Bucari-Tovo
ed66019d9a Cleanup 2022-05-24 18:24:53 -06:00
Michael Bucari-Tovo
bc0009be6c Use event return value instead of passing a set delegate. 2022-05-24 15:47:30 -06:00
Michael Bucari-Tovo
c88f47eed4 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 15:34:09 -06:00
Michael Bucari-Tovo
59de048ced Error handling network error. 2022-05-24 15:33:52 -06:00
Robert McRackan
7987dfb819 Rename 'liberate visible' menu items. Similar names are error-prone 2022-05-24 15:45:56 -04:00
Robert McRackan
1b101106e7 Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-24 15:32:42 -04:00
rmcrackan
7b75955aec Merge pull request #257 from Mbucari/master
Fix hang on grid update
2022-05-24 15:32:29 -04:00
Michael Bucari-Tovo
8f5467e6ca Revert stupid change. 2022-05-24 13:30:39 -06:00
Michael Bucari-Tovo
28764f92b9 Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-24 13:16:56 -06:00
Michael Bucari-Tovo
777dfe4c62 Fix hang on grid update 2022-05-24 13:16:44 -06:00
Robert McRackan
0878a704d9 search engine: podcast and episode should allow plural 2022-05-24 15:07:53 -04:00
Robert McRackan
f880897542 Increm version 2022-05-24 14:04:53 -04:00
rmcrackan
b37472a954 Merge pull request #255 from Mbucari/master
Implemented Episode grouping and refactored ProductsGrid
2022-05-24 13:59:12 -04:00
Michael Bucari-Tovo
68735a45dd Change episode color 2022-05-24 11:52:33 -06:00
Michael Bucari-Tovo
e26deb9092 Address comments 2022-05-24 11:15:41 -06:00
Michael Bucari-Tovo
43d6ea82cd Change failure behavior to match previous implementation 2022-05-24 09:17:09 -06:00
Mbucari
db1aa495ac Merge branch 'rmcrackan:master' into master 2022-05-24 08:48:32 -06:00
Michael Bucari-Tovo
ee62d9ae8d Attempt to fix app hang on LogMe event 2022-05-24 07:36:17 -06:00
Robert McRackan
4001124cfa AudibleApi. Better logging around getting pdf url 2022-05-24 09:03:43 -04:00
Michael Bucari-Tovo
43a4d0d1d7 Cleanup 2022-05-23 22:24:45 -06:00
Michael Bucari-Tovo
632b432b7c Revert to old column indexing 2022-05-23 22:21:37 -06:00
Michael Bucari-Tovo
e778c7a59d Create GridView namespace 2022-05-23 21:34:43 -06:00
Michael Bucari-Tovo
d71cdecd35 Refactoring and addressing comments 2022-05-23 21:20:26 -06:00
Michael Bucari-Tovo
4a82541ffd Fix error while removing filter on a sorted binding list 2022-05-23 17:46:55 -06:00
Michael Bucari-Tovo
f29dff3386 Fix filtering bug 2022-05-23 17:22:02 -06:00
Michael Bucari-Tovo
718d21f6cb NotifyPropertyChanged series on update 2022-05-23 16:42:05 -06:00
Michael Bucari-Tovo
440550ded9 Add binding source at design time 2022-05-23 16:35:18 -06:00
Michael Bucari-Tovo
593fe57ea1 Refactor ProductsGrid 2022-05-23 15:29:26 -06:00
Michael Bucari-Tovo
e8a320dac9 Add grid categories 2022-05-22 20:00:41 -06:00
Michael Bucari-Tovo
3cb43e5d3e Improve display 2022-05-22 20:00:06 -06:00
Robert McRackan
f86bdba3c3 Test in-place upgrade 2022-05-20 16:26:58 -04:00
Robert McRackan
98c3940297 New feature ( #153 ): in-place upgrade 2022-05-20 16:20:28 -04:00
Robert McRackan
b9e789bbcf Merge branch 'master' of https://github.com/rmcrackan/Libation 2022-05-20 14:22:09 -04:00
Robert McRackan
a108846731 Creating migrations shouldn't use file with conflicting name 2022-05-20 14:19:58 -04:00
rmcrackan
0b4ce8d6e7 Merge pull request #254 from Mbucari/master
Update installer
2022-05-18 23:02:58 -04:00
Mbucari
42df61b7dd Merge branch 'rmcrackan:master' into master 2022-05-18 17:33:10 -06:00
Michael Bucari-Tovo
6b46fa4cbc Use package installer 2022-05-18 17:32:53 -06:00
Robert McRackan
c0762eba18 Minor bug fix 2022-05-18 14:54:48 -04:00
rmcrackan
036fb848e1 Merge pull request #253 from Mbucari/master
Revert that unnecessary change
2022-05-18 14:53:47 -04:00
Mbucari
7198ae9025 Merge branch 'rmcrackan:master' into master 2022-05-18 12:49:31 -06:00
Michael Bucari-Tovo
d2822b06aa Revert "restore the old functionality to the stoplight"
This reverts commit 3648de3a8d.
2022-05-18 12:48:57 -06:00
Robert McRackan
17feca28b9 increm version 2022-05-18 14:48:46 -04:00
rmcrackan
898d38cb6a Merge pull request #252 from Mbucari/master
Fixed Issue #251
2022-05-18 14:47:40 -04:00
Michael Bucari-Tovo
95a99a2f0b Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-18 12:35:18 -06:00
Michael Bucari-Tovo
29a1e8ad34 MP3 settings always applicable for converting existing m4b 2022-05-18 12:35:12 -06:00
Mbucari
19f3a4f266 Update DownloadDecryptBook.cs 2022-05-18 12:31:32 -06:00
Michael Bucari-Tovo
12ddbc308a Fixed multipart book hanging 2022-05-18 12:29:16 -06:00
Mbucari
999bc7604e Merge branch 'rmcrackan:master' into master 2022-05-18 09:47:23 -06:00
Michael Bucari-Tovo
3648de3a8d restore the old functionality to the stoplight 2022-05-18 09:47:09 -06:00
Robert McRackan
051fa0a28f Bug fix #250 : recent refactor introduced a race condition for db creation on initial install. Moved db creation to before all other init/config is called 2022-05-18 08:13:18 -04:00
Robert McRackan
72e667e825 update dependencies 2022-05-17 17:13:20 -04:00
Robert McRackan
5ed59b41b5 fix image scaling bug when when scanning 2022-05-17 16:38:49 -04:00
Robert McRackan
c7c0d1632e Improve how highlighted index works post-filtering 2022-05-17 12:59:30 -04:00
Robert McRackan
2dc73acd20 Bug fix: incomplete refresh 2022-05-17 09:53:23 -04:00
Robert McRackan
ed71668c48 Reverted -- I might have been too hasty removing the GridEntry events 2022-05-17 08:08:53 -04:00
Robert McRackan
801e154d15 post-refactor clean up 2022-05-17 07:56:34 -04:00
rmcrackan
a89b07394f Merge pull request #249 from Mbucari/master
Add FilterableSortableBindingList to handle filtering the DataGridView
2022-05-16 22:08:31 -04:00
Mbucari
982f9b7c58 Merge branch 'rmcrackan:master' into master 2022-05-16 16:38:03 -06:00
Michael Bucari-Tovo
789b9207b5 Use that fancy patterm matching 2022-05-16 15:49:02 -06:00
Michael Bucari-Tovo
133dbb7471 Update Dinah 2022-05-16 15:11:21 -06:00
Robert McRackan
5d3ec493cd update dependencies 2022-05-16 17:06:59 -04:00
Michael Bucari-Tovo
6d7f234497 Remove unnecessary base form 2022-05-16 14:32:59 -06:00
Michael Bucari-Tovo
29a50bb640 typo 2022-05-16 14:31:03 -06:00
Michael Bucari-Tovo
843fddabde Changes discussed in email 2022-05-16 14:27:34 -06:00
Michael Bucari-Tovo
109ce0dd1f overwrite cached state 2022-05-16 14:26:43 -06:00
Robert McRackan
42508a82a0 txt file rename 2022-05-16 16:04:53 -04:00
Michael Bucari-Tovo
d860d39f5f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-16 13:16:58 -06:00
Michael Bucari-Tovo
15396c611a Add documentation 2022-05-16 13:16:50 -06:00
Mbucari
41c4b12ae1 Merge branch 'rmcrackan:master' into master 2022-05-16 13:16:25 -06:00
Michael Bucari-Tovo
e51c30462f Revert "Use new ProcessQueue"
This reverts commit 9b5df99a61.
2022-05-16 13:16:11 -06:00
Michael Bucari-Tovo
9b5df99a61 Use new ProcessQueue 2022-05-16 12:56:15 -06:00
Michael Bucari-Tovo
3535156ea5 Edit 2022-05-16 12:47:50 -06:00
Robert McRackan
577145096d * GridEntry.DownloadBook is no longer called. it was the only one calling UpdateLiberatedStatus(true) or using DownloadInProgress flag
* cleaned up unused code, old forms, ProcessorAutomationController...
* what's left of LogMe and ProcessorAutomationController should be moved eventually
2022-05-16 14:44:17 -04:00
Michael Bucari-Tovo
89059510fd More logical naming 2022-05-16 12:37:56 -06:00
Michael Bucari-Tovo
aabc14c639 Make AllItems a method 2022-05-16 12:12:34 -06:00
Michael Bucari-Tovo
c28872544c Don't call concat for every book. 2022-05-16 12:10:54 -06:00
Michael Bucari-Tovo
7b8a4e4d72 Simplify filtering 2022-05-16 12:06:56 -06:00
Michael Bucari-Tovo
5dcdf670be Simplify RemoveFilter 2022-05-16 11:58:36 -06:00
Michael Bucari-Tovo
9721890a3c Update documentation 2022-05-16 11:50:11 -06:00
Michael Bucari-Tovo
1b9c4cfc23 Remove unused usings 2022-05-16 11:47:34 -06:00
Michael Bucari-Tovo
98a552e9af Optimization 2022-05-16 11:46:42 -06:00
Michael Bucari-Tovo
e1e265a101 Don't filter after every insert 2022-05-16 11:38:24 -06:00
Robert McRackan
b60a854de0 Formattable UI labels 2022-05-16 13:34:49 -04:00
Michael Bucari-Tovo
d1bddeccc8 Implement filtering in the sortable binding list. 2022-05-16 11:16:33 -06:00
Robert McRackan
0a106e64d8 liberate visible to use new process queue 2022-05-16 08:20:40 -04:00
Michael Bucari-Tovo
91d6181aec Better naming 2022-05-15 20:15:54 -06:00
Michael Bucari-Tovo
255c0a3359 Move filtering into SyncBindingSource 2022-05-15 19:58:59 -06:00
Robert McRackan
3a5ef999f0 Bug fix: fatal exception if no large picture 2022-05-15 15:38:36 -04:00
rmcrackan
983aa845d6 Merge pull request #247 from Mbucari/master
Fixed scaling issue
2022-05-15 15:21:05 -04:00
Robert McRackan
d1779726e6 New __ARCHITECTURE NOTES.txt incl. MVVM comments 2022-05-15 15:13:25 -04:00
Michael Bucari-Tovo
8e23062d0e Fix scaling for all display scalings 2022-05-15 13:12:46 -06:00
Michael Bucari-Tovo
7efbfffd99 Fixed scaling issue. 2022-05-15 12:47:39 -06:00
rmcrackan
ff4b2d2ecc Merge pull request #244 from Mbucari/master
New Processing Queue
2022-05-15 14:25:52 -04:00
Michael Bucari-Tovo
e079be0ad7 Make scrll look more natural when removing items from control 2022-05-15 11:27:49 -06:00
Michael Bucari-Tovo
a8a54aa443 Revert "Make scrll look more natural when removing items from control"
This reverts commit 88cbcf6baf.
2022-05-15 11:26:07 -06:00
Michael Bucari-Tovo
88cbcf6baf Make scrll look more natural when removing items from control 2022-05-15 11:25:33 -06:00
Michael Bucari-Tovo
8d6d26c9d2 Improve logging 2022-05-15 11:16:41 -06:00
Michael Bucari-Tovo
a490df0f7e Fix possible index range error 2022-05-15 11:10:37 -06:00
Michael Bucari-Tovo
a46041c958 More useful logging 2022-05-15 09:58:36 -06:00
Michael Bucari-Tovo
0a6a78bc58 Revert "More useful logging"
This reverts commit c9e850515e.
2022-05-15 09:56:56 -06:00
Michael Bucari-Tovo
c9e850515e More useful logging 2022-05-15 09:56:46 -06:00
Michael Bucari-Tovo
0ff8da2cf0 Add await and make cancel async 2022-05-15 09:30:44 -06:00
Michael Bucari-Tovo
c0ef3ccbea Tiny bugfix 2022-05-15 09:00:52 -06:00
Michael Bucari-Tovo
1ab628dee8 Better invocation. Post instead of Send 2022-05-14 23:45:13 -06:00
Michael Bucari-Tovo
b24df24b10 Detect Conversion cancelled 2022-05-14 23:44:15 -06:00
Michael Bucari-Tovo
341678d979 Remove my testing code. oops. 2022-05-14 20:47:13 -06:00
Michael Bucari-Tovo
49d10273a6 Add button to hide queue 2022-05-14 20:44:53 -06:00
Michael Bucari-Tovo
5b05c018d5 Remove ValidationFail books from queue display. Nothing to see there. 2022-05-14 20:00:23 -06:00
Michael Bucari-Tovo
d18d8c0ba4 Filter 2022-05-14 16:23:34 -06:00
Michael Bucari-Tovo
84a8fb0074 Minor refactor 2022-05-14 16:13:19 -06:00
Michael Bucari-Tovo
a40fb7f4bd Colorsto variables 2022-05-14 15:57:36 -06:00
Michael Bucari-Tovo
84eb3a3508 Remove debug button 2022-05-14 15:33:06 -06:00
Michael Bucari-Tovo
73a5d76503 Make thread safe and integrate with Libation UI 2022-05-14 14:39:46 -06:00
Michael Bucari-Tovo
50c35ed519 Change log to gridview and new INotifyPropertyChanged event 2022-05-14 13:52:54 -06:00
Michael Bucari-Tovo
a7b7e3efea Converted to INotifyPropertyChanged for more targeted view update 2022-05-14 13:52:10 -06:00
Michael Bucari-Tovo
88e892196f Merge branch 'master' of https://github.com/Mbucari/Libation 2022-05-14 11:20:30 -06:00
Michael Bucari-Tovo
7f08da96bb Documentation and organization 2022-05-14 11:20:19 -06:00
Mbucari
193f24768e Merge branch 'rmcrackan:master' into master 2022-05-14 11:15:23 -06:00
Michael Bucari-Tovo
a8bca3de98 Fix progressbar wiggling 2022-05-14 11:11:51 -06:00
Michael Bucari-Tovo
9692a802d0 Update documentation and add parameters 2022-05-14 11:11:20 -06:00
Robert McRackan
28a8b2e685 Revert: only call notifyPropertyChanged if actually set to new value 2022-05-14 12:34:01 -04:00
Michael Bucari-Tovo
3c9121b4af Improve scroll visualization 2022-05-14 10:03:57 -06:00
Michael Bucari-Tovo
dec1035258 Minor UI tweak 2022-05-14 04:11:44 -06:00
Michael Bucari-Tovo
9d81c86c1b Increase buffer size 2022-05-14 04:10:54 -06:00
Michael Bucari-Tovo
eeb4f4681a Saved 2022-05-14 03:16:48 -06:00
Michael Bucari-Tovo
676af0210b Finalized ProcessBookControl 2022-05-14 02:54:32 -06:00
Michael Bucari-Tovo
77c6a2890b Finalized VirtualFlowControl 2022-05-14 02:54:09 -06:00
Michael Bucari-Tovo
c39e748749 Finialized TrackedQueue 2022-05-14 01:33:05 -06:00
Mbucari
36e5a6ac8d Merge branch 'rmcrackan:master' into master 2022-05-13 21:00:56 -06:00
Robert McRackan
9bdcaa5eaa only call notifyPropertyChanged if actually set to new value 2022-05-13 16:30:46 -04:00
Mbucari
5511004db8 Merge branch 'rmcrackan:master' into master 2022-05-13 14:09:29 -06:00
Robert McRackan
0e46cdb514 refactor Form1. too much in 1 file 2022-05-13 13:39:49 -04:00
Mbucari
b028899949 Merge branch 'rmcrackan:master' into master 2022-05-13 11:12:37 -06:00
Robert McRackan
55285427f1 Add series in default search 2022-05-13 11:42:08 -04:00
Michael Bucari-Tovo
763a6cb31a Added VirtualFlowControl and BookQueue 2022-05-13 00:21:41 -06:00
Robert McRackan
24cb1aa84f remove legacy references 2022-05-12 13:35:08 -04:00
rmcrackan
886aa4938d Merge pull request #243 from Mbucari/master
Minor mods for future UI changes
2022-05-12 13:30:30 -04:00
Michael Bucari-Tovo
8871651549 Change namespace/folder name 2022-05-12 11:17:14 -06:00
Michael Bucari-Tovo
2ae8ef87d9 Remove unised 2022-05-12 11:06:53 -06:00
Michael Bucari-Tovo
de4fbe05f7 Remove old holdover 2022-05-12 11:01:41 -06:00
Michael Bucari-Tovo
b8abed37c2 Merged popout with main brainch. 2022-05-12 10:52:55 -06:00
Mbucari
255e26435c Merge branch 'rmcrackan:master' into master 2022-05-12 10:32:05 -06:00
Michael Bucari-Tovo
9e0550619b Merging 2022-05-12 10:31:50 -06:00
Michael Bucari-Tovo
5c171fd0f0 Merging 2022-05-12 10:29:44 -06:00
Michael Bucari-Tovo
3dd3b710b7 merge fix 2022-05-12 10:17:29 -06:00
Michael Bucari-Tovo
3c0485cfa9 Prepare Form1 for new docking queue 2022-05-12 00:10:13 -06:00
Michael Bucari-Tovo
d5ba405de0 MatchCurrent 2022-05-11 23:44:33 -06:00
Michael Bucari-Tovo
71b8bca86d Revert "Revert "Changes for dockable process""
This reverts commit 6d6434b4d4.
2022-05-11 21:12:18 -06:00
Mbucari
c53b9eabd6 Merge branch 'rmcrackan:master' into master 2022-05-11 21:11:44 -06:00
Michael Bucari-Tovo
6d6434b4d4 Revert "Changes for dockable process"
This reverts commit a447e88b86.
2022-05-11 21:11:25 -06:00
Michael Bucari-Tovo
a447e88b86 Changes for dockable process 2022-05-11 21:10:02 -06:00
Michael Bucari-Tovo
e2d2e00913 Fix conflict? 2022-05-11 21:07:32 -06:00
Michael Bucari-Tovo
cbfea37b3a Fix conflict 2022-05-11 21:06:01 -06:00
Michael Bucari-Tovo
d6de647974 Fix conflict 2022-05-11 21:04:11 -06:00
Michael Bucari-Tovo
dad36c73e5 Expose grid entries 2022-05-11 18:45:57 -06:00
Michael Bucari-Tovo
936a1d60a0 Minor mods for future UI changes 2022-05-11 18:36:48 -06:00
125 changed files with 8214 additions and 4622 deletions

View File

@@ -40,7 +40,7 @@ Libation's advanced searching is built on the powerful Lucene search engine. Sim
* Full official guide: https://lucene.apache.org/core/2_9_4/queryparsersyntax.html
* Tons of search fields, specific to audiobooks
* Synonyms so you don't have to memorize magic words. Eg: author and author**s** will both work
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptions.png)
* Click [?] button for a full list of search fields and synonyms ![Filter options](images/FilterOptionsButton.png)
* Search by tag like \[this\]
* When tags have an underscore you can use part of the tag. This is useful for quick categories. The below examples make this more clear.

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean" Version="0.4.6" />
<PackageReference Include="AAXClean" Version="0.4.7" />
<PackageReference Include="AAXClean.Codecs" Version="0.2.7" />
</ItemGroup>

View File

@@ -20,7 +20,7 @@ namespace AaxDecrypter
public event EventHandler<TimeSpan> DecryptTimeRemaining;
public event EventHandler<string> FileCreated;
protected bool IsCanceled { get; set; }
public bool IsCanceled { get; set; }
protected string OutputFileName { get; private set; }
protected DownloadOptions DownloadOptions { get; }

View File

@@ -9,435 +9,442 @@ using System.Threading;
namespace AaxDecrypter
{
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
{
private Uri baseAddress;
public Uri Uri
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
}
public CookieCollection GetCookies()
{
return GetCookies(Uri);
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable
{
public event EventHandler Updated;
#region Public Properties
/// <summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
/// <summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
#endregion
#region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion
#region Constants
//Download buffer size
private const int DOWNLOAD_BUFF_SZ = 4 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
#endregion
#region Constructor
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
Position = WritePosition
};
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update()
{
RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri);
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
}
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength)
{
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
new Thread(() => DownloadFile())
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
}
/// <summary>
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary>
private void DownloadFile()
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
do
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
}
internal class CookieContainerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => true;
[JsonIgnore]
public override bool CanSeek => true;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (!hasBegunDownloading)
BeginDownloading();
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
/// <summary>
/// A <see cref="CookieContainer"/> for a single Uri.
/// </summary>
public class SingleUriCookieContainer : CookieContainer
{
private Uri baseAddress;
public Uri Uri
{
get => baseAddress;
set
{
baseAddress = new UriBuilder(value.Scheme, value.Host).Uri;
}
}
public CookieCollection GetCookies()
{
return GetCookies(Uri);
}
}
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
public class NetworkFileStream : Stream, IUpdatable
{
public event EventHandler Updated;
#region Public Properties
/// <summary>
/// Location to save the downloaded data.
/// </summary>
[JsonProperty(Required = Required.Always)]
public string SaveFilePath { get; }
/// <summary>
/// Http(s) address of the file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public Uri Uri { get; private set; }
/// <summary>
/// All cookies set by caller or by the remote server.
/// </summary>
[JsonProperty(Required = Required.Always)]
public SingleUriCookieContainer CookieContainer { get; }
/// <summary>
/// Http headers to be sent to the server with the request.
/// </summary>
[JsonProperty(Required = Required.Always)]
public WebHeaderCollection RequestHeaders { get; private set; }
/// <summary>
/// The position in <see cref="SaveFilePath"/> that has been written and flushed to disk.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long WritePosition { get; private set; }
/// <summary>
/// The total length of the <see cref="Uri"/> file to download.
/// </summary>
[JsonProperty(Required = Required.Always)]
public long ContentLength { get; private set; }
#endregion
#region Private Properties
private HttpWebRequest HttpRequest { get; set; }
private FileStream _writeFile { get; }
private FileStream _readFile { get; }
private Stream _networkStream { get; set; }
private bool hasBegunDownloading { get; set; }
public bool IsCancelled { get; private set; }
private EventWaitHandle downloadEnded { get; set; }
private EventWaitHandle downloadedPiece { get; set; }
#endregion
#region Constants
//Download buffer size
private const int DOWNLOAD_BUFF_SZ = 32 * 1024;
//NetworkFileStream will flush all data in _writeFile to disk after every
//DATA_FLUSH_SZ bytes are written to the file stream.
private const int DATA_FLUSH_SZ = 1024 * 1024;
#endregion
#region Constructor
/// <summary>
/// A resumable, simultaneous file downloader and reader.
/// </summary>
/// <param name="saveFilePath">Path to a location on disk to save the downloaded data from <paramref name="uri"/></param>
/// <param name="uri">Http(s) address of the file to download.</param>
/// <param name="writePosition">The position in <paramref name="uri"/> to begin downloading.</param>
/// <param name="requestHeaders">Http headers to be sent to the server with the <see cref="HttpWebRequest"/>.</param>
/// <param name="cookies">A <see cref="SingleUriCookieContainer"/> with cookies to send with the <see cref="HttpWebRequest"/>. It will also be populated with any cookies set by the server. </param>
public NetworkFileStream(string saveFilePath, Uri uri, long writePosition = 0, WebHeaderCollection requestHeaders = null, SingleUriCookieContainer cookies = null)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(saveFilePath, nameof(saveFilePath));
ArgumentValidator.EnsureNotNullOrWhiteSpace(uri?.AbsoluteUri, nameof(uri));
ArgumentValidator.EnsureGreaterThan(writePosition, nameof(writePosition), -1);
if (!Directory.Exists(Path.GetDirectoryName(saveFilePath)))
throw new ArgumentException($"Specified {nameof(saveFilePath)} directory \"{Path.GetDirectoryName(saveFilePath)}\" does not exist.");
SaveFilePath = saveFilePath;
Uri = uri;
WritePosition = writePosition;
RequestHeaders = requestHeaders ?? new WebHeaderCollection();
CookieContainer = cookies ?? new SingleUriCookieContainer { Uri = uri };
_writeFile = new FileStream(SaveFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)
{
Position = WritePosition
};
_readFile = new FileStream(SaveFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
SetUriForSameFile(uri);
}
#endregion
#region Downloader
/// <summary>
/// Update the <see cref="JsonFilePersister"/>.
/// </summary>
private void Update()
{
RequestHeaders = HttpRequest.Headers;
Updated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Set a different <see cref="System.Uri"/> to the same file targeted by this instance of <see cref="NetworkFileStream"/>
/// </summary>
/// <param name="uriToSameFile">New <see cref="System.Uri"/> host must match existing host.</param>
public void SetUriForSameFile(Uri uriToSameFile)
{
ArgumentValidator.EnsureNotNullOrWhiteSpace(uriToSameFile?.AbsoluteUri, nameof(uriToSameFile));
if (uriToSameFile.Host != Uri.Host)
throw new ArgumentException($"New uri to the same file must have the same host.\r\n Old Host :{Uri.Host}\r\nNew Host: {uriToSameFile.Host}");
if (hasBegunDownloading)
throw new InvalidOperationException("Cannot change Uri after download has started.");
Uri = uriToSameFile;
HttpRequest = WebRequest.CreateHttp(Uri);
HttpRequest.CookieContainer = CookieContainer;
HttpRequest.Headers = RequestHeaders;
//If NetworkFileStream is resuming, Header will already contain a range.
HttpRequest.Headers.Remove("Range");
HttpRequest.AddRange(WritePosition);
}
/// <summary>
/// Begins downloading <see cref="Uri"/> to <see cref="SaveFilePath"/> in a background thread.
/// </summary>
private void BeginDownloading()
{
downloadEnded = new EventWaitHandle(false, EventResetMode.ManualReset);
if (ContentLength != 0 && WritePosition == ContentLength)
{
hasBegunDownloading = true;
downloadEnded.Set();
return;
}
if (ContentLength != 0 && WritePosition > ContentLength)
throw new WebException($"Specified write position (0x{WritePosition:X10}) is larger than {nameof(ContentLength)} (0x{ContentLength:X10}).");
var response = HttpRequest.GetResponse() as HttpWebResponse;
if (response.StatusCode != HttpStatusCode.PartialContent)
throw new WebException($"Server at {Uri.Host} responded with unexpected status code: {response.StatusCode}.");
//Content length is the length of the range request, and it is only equal
//to the complete file length if requesting Range: bytes=0-
if (WritePosition == 0)
ContentLength = response.ContentLength;
_networkStream = response.GetResponseStream();
downloadedPiece = new EventWaitHandle(false, EventResetMode.AutoReset);
//Download the file in the background.
new Thread(() => DownloadFile())
{ IsBackground = true }
.Start();
hasBegunDownloading = true;
return;
}
/// <summary>
/// Downlod <see cref="Uri"/> to <see cref="SaveFilePath"/>.
/// </summary>
private void DownloadFile()
{
var downloadPosition = WritePosition;
var nextFlush = downloadPosition + DATA_FLUSH_SZ;
var buff = new byte[DOWNLOAD_BUFF_SZ];
try
{
do
{
var bytesRead = _networkStream.Read(buff, 0, DOWNLOAD_BUFF_SZ);
_writeFile.Write(buff, 0, bytesRead);
downloadPosition += bytesRead;
if (downloadPosition > nextFlush)
{
_writeFile.Flush();
WritePosition = downloadPosition;
Update();
nextFlush = downloadPosition + DATA_FLUSH_SZ;
downloadedPiece.Set();
}
} while (downloadPosition < ContentLength && !IsCancelled);
_writeFile.Close();
_networkStream.Close();
WritePosition = downloadPosition;
Update();
downloadedPiece.Set();
downloadEnded.Set();
if (!IsCancelled && WritePosition < ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is less than {nameof(ContentLength)} (0x{ContentLength:X10}).");
if (WritePosition > ContentLength)
throw new WebException($"Downloaded size (0x{WritePosition:X10}) is greater than {nameof(ContentLength)} (0x{ContentLength:X10}).");
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "An error was encountered while downloading {Uri}", Uri);
IsCancelled = true;
}
}
#endregion
#region Json Connverters
public static JsonSerializerSettings GetJsonSerializerSettings()
{
var settings = new JsonSerializerSettings();
settings.Converters.Add(new CookieContainerConverter());
settings.Converters.Add(new WebHeaderCollectionConverter());
return settings;
}
internal class CookieContainerConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(SingleUriCookieContainer);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new SingleUriCookieContainer()
{
Uri = new Uri(jObj["Uri"].Value<string>()),
Capacity = jObj["Capacity"].Value<int>(),
MaxCookieSize = jObj["MaxCookieSize"].Value<int>(),
PerDomainCapacity = jObj["PerDomainCapacity"].Value<int>()
};
var cookieList = jObj["Cookies"].ToList();
foreach (var cookie in cookieList)
{
result.Add(
new Cookie
{
Comment = cookie["Comment"].Value<string>(),
HttpOnly = cookie["HttpOnly"].Value<bool>(),
Discard = cookie["Discard"].Value<bool>(),
Domain = cookie["Domain"].Value<string>(),
Expired = cookie["Expired"].Value<bool>(),
Expires = cookie["Expires"].Value<DateTime>(),
Name = cookie["Name"].Value<string>(),
Path = cookie["Path"].Value<string>(),
Port = cookie["Port"].Value<string>(),
Secure = cookie["Secure"].Value<bool>(),
Value = cookie["Value"].Value<string>(),
Version = cookie["Version"].Value<int>(),
});
}
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var cookies = value as SingleUriCookieContainer;
var obj = (JObject)JToken.FromObject(value);
var container = cookies.GetCookies();
var propertyNames = container.Select(c => JToken.FromObject(c));
obj.AddFirst(new JProperty("Cookies", new JArray(propertyNames)));
obj.WriteTo(writer);
}
}
internal class WebHeaderCollectionConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
=> objectType == typeof(WebHeaderCollection);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var jObj = JObject.Load(reader);
var result = new WebHeaderCollection();
foreach (var kvp in jObj)
result.Add(kvp.Key, kvp.Value.Value<string>());
return result;
}
public override bool CanWrite => true;
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var jObj = new JObject();
var type = value.GetType();
var headers = value as WebHeaderCollection;
var jHeaders = headers.AllKeys.Select(k => new JProperty(k, headers[k]));
jObj.Add(jHeaders);
jObj.WriteTo(writer);
}
}
#endregion
#region Download Stream Reader
[JsonIgnore]
public override bool CanRead => true;
[JsonIgnore]
public override bool CanSeek => true;
[JsonIgnore]
public override bool CanWrite => false;
[JsonIgnore]
public override long Length
{
get
{
if (!hasBegunDownloading)
BeginDownloading();
return ContentLength;
}
}
[JsonIgnore]
public override long Position { get => _readFile.Position; set => Seek(value, SeekOrigin.Begin); }
[JsonIgnore]
public override bool CanTimeout => false;
[JsonIgnore]
public override int ReadTimeout { get => base.ReadTimeout; set => base.ReadTimeout = value; }
[JsonIgnore]
public override int WriteTimeout { get => base.WriteTimeout; set => base.WriteTimeout = value; }
public override void Flush() => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override int Read(byte[] buffer, int offset, int count)
{
if (!hasBegunDownloading)
BeginDownloading();
var toRead = Math.Min(count, Length - Position);
WaitToPosition(Position + toRead);
return _readFile.Read(buffer, offset, count);
}
public override long Seek(long offset, SeekOrigin origin)
{
var newPosition = origin switch
{
SeekOrigin.Current => Position + offset,
SeekOrigin.End => ContentLength + offset,
_ => offset,
};
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
WaitToPosition(newPosition);
return _readFile.Position = newPosition;
}
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
/// <summary>
/// Blocks until the file has downloaded to at least <paramref name="requiredPosition"/>, then returns.
/// </summary>
/// <param name="requiredPosition">The minimum required flished data length in <see cref="SaveFilePath"/>.</param>
private void WaitToPosition(long requiredPosition)
{
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
while (requiredPosition > WritePosition && !IsCancelled && hasBegunDownloading && !downloadedPiece.WaitOne(1000)) ;
}
public override void Close()
{
IsCancelled = true;
public override void Close()
{
IsCancelled = true;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
while (downloadEnded is not null && !downloadEnded.WaitOne(1000)) ;
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
_readFile.Close();
_writeFile.Close();
_networkStream?.Close();
Update();
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
#endregion
~NetworkFileStream()
{
downloadEnded?.Close();
downloadedPiece?.Close();
}
}
}

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0-windows</TargetFramework>
<Version>7.5.0.1</Version>
<Version>7.10.0.1</Version>
</PropertyGroup>
<ItemGroup>
@@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AudibleUtilities\AudibleUtilities.csproj" />
</ItemGroup>

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO;
@@ -137,14 +138,19 @@ namespace AppScaffolding
if (!config.Exists(nameof(config.DownloadCoverArt)))
config.DownloadCoverArt = true;
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
config.AutoDownloadEpisodes = false;
}
/// <summary>Initialize logging. Run after migration</summary>
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
public static void RunPostMigrationScaffolding(Configuration config)
{
ensureSerilogConfig(config);
configureLogging(config);
logStartupState(config);
wireUpSystemEvents(config);
}
private static void ensureSerilogConfig(Configuration config)
@@ -282,22 +288,26 @@ namespace AppScaffolding
});
}
public static (bool hasUpgrade, string zipUrl, string htmlUrl, string zipName) GetLatestRelease()
private static void wireUpSystemEvents(Configuration configuration)
{
(bool, string, string, string) isFalse = (false, null, null, null);
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books);
}
public static UpgradeProperties GetLatestRelease()
{
// timed out
var latest = getLatestRelease(TimeSpan.FromSeconds(10));
if (latest is null)
return isFalse;
return null;
var latestVersionString = latest.TagName.Trim('v');
if (!Version.TryParse(latestVersionString, out var latestRelease))
return isFalse;
return null;
// we're up to date
if (latestRelease <= BuildVersion)
return isFalse;
return null;
// we have an update
var zip = latest.Assets.FirstOrDefault(a => a.BrowserDownloadUrl.EndsWith(".zip"));
@@ -310,7 +320,7 @@ namespace AppScaffolding
zipUrl
});
return (true, zipUrl, latest.HtmlUrl, zip.Name);
return new(zipUrl, latest.HtmlUrl, zip.Name, latestRelease);
}
private static Octokit.Release getLatestRelease(TimeSpan timeout)
{

View File

@@ -0,0 +1,6 @@
using System;
namespace AppScaffolding
{
public record UpgradeProperties(string ZipUrl, string HtmlUrl, string ZipName, Version LatestRelease);
}

View File

@@ -253,11 +253,7 @@ namespace ApplicationServices
#endregion
// call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange()
{
SearchEngineCommands.FullReIndex();
LibrarySizeChanged?.Invoke(null, null);
}
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null);
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged;
@@ -265,9 +261,47 @@ namespace ApplicationServices
/// <summary>
/// Occurs when the size of the library does not change but book(s) details do. Especially when <see cref="UserDefinedItem.Tags"/>, <see cref="UserDefinedItem.BookStatus"/>, or <see cref="UserDefinedItem.PdfStatus"/> changed values are successfully persisted.
/// </summary>
public static event EventHandler BookUserDefinedItemCommitted;
public static event EventHandler<IEnumerable<Book>> BookUserDefinedItemCommitted;
#region Update book details
public static int UpdateBookStatus(this Book book, LiberatedStatus bookStatus)
{
book.UserDefinedItem.BookStatus = bookStatus;
return UpdateUserDefinedItem(book);
}
public static int UpdatePdfStatus(this Book book, LiberatedStatus pdfStatus)
{
book.UserDefinedItem.PdfStatus = pdfStatus;
return UpdateUserDefinedItem(book);
}
public static int UpdateBook(
this Book book,
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null)
=> UpdateBooks(tags, bookStatus, pdfStatus, book);
public static int UpdateBooks(
string tags = null,
LiberatedStatus? bookStatus = null,
LiberatedStatus? pdfStatus = null,
params Book[] books)
{
foreach (var book in books)
{
// blank tags are expected. null tags are not
if (tags is not null && book.UserDefinedItem.Tags != tags)
book.UserDefinedItem.Tags = tags;
if (bookStatus is not null && book.UserDefinedItem.BookStatus != bookStatus.Value)
book.UserDefinedItem.BookStatus = bookStatus.Value;
// even though PdfStatus is nullable, there's no case where we'd actually overwrite with null
if (pdfStatus is not null && book.UserDefinedItem.PdfStatus != pdfStatus.Value)
book.UserDefinedItem.PdfStatus = pdfStatus.Value;
}
return UpdateUserDefinedItem(books);
}
public static int UpdateUserDefinedItem(params Book[] books) => UpdateUserDefinedItem(books.ToList());
public static int UpdateUserDefinedItem(IEnumerable<Book> books)
{
@@ -283,23 +317,8 @@ namespace ApplicationServices
context.Attach(book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
var qtyChanges = context.SaveChanges();
if (qtyChanges == 0)
return 0;
// semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (qtyChanges > 15)
SearchEngineCommands.FullReIndex();
else
{
foreach (var book in books)
{
SearchEngineCommands.UpdateLiberatedStatus(book);
SearchEngineCommands.UpdateBookTags(book);
}
}
BookUserDefinedItemCommitted?.Invoke(null, null);
if (qtyChanges > 0)
BookUserDefinedItemCommitted?.Invoke(null, books);
return qtyChanges;
}
@@ -322,7 +341,14 @@ namespace ApplicationServices
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
{
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
public bool HasPendingBooks => PendingBooks > 0;
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
}
public static LibraryStats GetCounts()
{
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();

View File

@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DataLayer;
using LibationSearchEngine;
@@ -7,51 +9,99 @@ namespace ApplicationServices
{
public static class SearchEngineCommands
{
public static void FullReIndex(SearchEngine engine = null)
{
engine ??= new SearchEngine();
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
public static SearchResultSet Search(string searchString) => performSearchEngineFunc_safe(e =>
#region Search
public static SearchResultSet Search(string searchString) => performSafeQuery(e =>
e.Search(searchString)
);
public static void UpdateBookTags(Book book) => performSearchEngineAction_safe(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
private static T performSafeQuery<T>(Func<SearchEngine, T> func)
{
var engine = new SearchEngine();
try
{
return func(engine);
}
catch (FileNotFoundException)
{
fullReIndex(engine);
return func(engine);
}
}
#endregion
public static EventHandler SearchEngineUpdated;
#region Update
private static bool isUpdating;
public static void UpdateBooks(IEnumerable<Book> books)
{
// Semi-arbitrary. At some point it's more worth it to do a full re-index than to do one offs.
// I did not benchmark before choosing the number here
if (books.Count() > 15)
FullReIndex();
else
{
foreach (var book in books)
{
UpdateLiberatedStatus(book);
UpdateBookTags(book);
}
}
}
public static void FullReIndex() => performSafeCommand(e =>
fullReIndex(e)
);
public static void UpdateLiberatedStatus(Book book) => performSearchEngineAction_safe(e =>
internal static void UpdateLiberatedStatus(Book book) => performSafeCommand(e =>
e.UpdateLiberatedStatus(book)
);
private static void performSearchEngineAction_safe(Action<SearchEngine> action)
internal static void UpdateBookTags(Book book) => performSafeCommand(e =>
e.UpdateTags(book.AudibleProductId, book.UserDefinedItem.Tags)
);
private static void performSafeCommand(Action<SearchEngine> action)
{
var engine = new SearchEngine();
try
{
action(engine);
update(action);
}
catch (FileNotFoundException)
{
FullReIndex(engine);
action(engine);
fullReIndex(new SearchEngine());
update(action);
}
}
private static T performSearchEngineFunc_safe<T>(Func<SearchEngine, T> func)
private static void update(Action<SearchEngine> action)
{
var engine = new SearchEngine();
if (action is null)
return;
// support nesting incl recursion
var prevIsUpdating = isUpdating;
try
{
return func(engine);
isUpdating = true;
action(new SearchEngine());
if (!prevIsUpdating)
SearchEngineUpdated?.Invoke(null, null);
}
catch (FileNotFoundException)
finally
{
FullReIndex(engine);
return func(engine);
isUpdating = prevIsUpdating;
}
}
private static void fullReIndex(SearchEngine engine)
{
var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library);
}
#endregion
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using AudibleApi;
using AudibleApi.Common;
@@ -118,31 +119,38 @@ namespace AudibleUtilities
private async Task<List<Item>> getItemsAsync(LibraryOptions libraryOptions, bool importEpisodes)
{
var items = new List<Item>();
#if DEBUG
//// this will not work for multi accounts
//var library_json = "library.json";
//library_json = System.IO.Path.GetFullPath(library_json);
//if (System.IO.File.Exists(library_json))
//{
// items = AudibleApi.Common.Converter.FromJson<List<Item>>(System.IO.File.ReadAllText(library_json));
//}
#endif
Serilog.Log.Logger.Debug("Begin initial library scan");
Serilog.Log.Logger.Debug("Beginning library scan.");
if (!items.Any())
items = await Api.GetAllLibraryItemsAsync(libraryOptions);
List<Task<List<Item>>> getChildEpisodesTasks = new();
Serilog.Log.Logger.Debug("Initial library scan complete. Begin episode scan");
int count = 0, maxConcurrentEpisodeScans = 5;
using SemaphoreSlim concurrencySemaphore = new(maxConcurrentEpisodeScans);
await manageEpisodesAsync(items, importEpisodes);
await foreach (var item in Api.GetLibraryItemAsyncEnumerable(libraryOptions))
{
if (item.IsEpisodes && importEpisodes)
{
//Get child episodes asynchronously and await all at the end
getChildEpisodesTasks.Add(getChildEpisodesAsync(concurrencySemaphore, item));
}
else if (!item.IsEpisodes)
items.Add(item);
Serilog.Log.Logger.Debug("Episode scan complete");
count++;
}
Serilog.Log.Logger.Debug("Library scan complete. Found {count} books and series. Waiting on {getChildEpisodesTasksCount} series episode scans to complete.", count, getChildEpisodesTasks.Count);
//await and add all episides from all parents
foreach (var epList in await Task.WhenAll(getChildEpisodesTasks))
items.AddRange(epList);
Serilog.Log.Logger.Debug("Completed library scan.");
#if DEBUG
//System.IO.File.WriteAllText(library_json, AudibleApi.Common.Converter.ToJson(items));
#endif
var validators = new List<IValidator>();
validators.AddRange(getValidators());
foreach (var v in validators)
@@ -156,55 +164,21 @@ namespace AudibleUtilities
}
#region episodes and podcasts
private async Task manageEpisodesAsync(List<Item> items, bool importEpisodes)
private async Task<List<Item>> getChildEpisodesAsync(SemaphoreSlim concurrencySemaphore, Item parent)
{
// add podcasts and episodes to list. If fail, don't let it de-rail the rest of the import
await concurrencySemaphore.WaitAsync();
try
{
// get parents
var parents = items.Where(i => i.IsEpisodes).ToList();
#if DEBUG
//var parentsDebug = parents.Select(i => i.ToJson()).Aggregate((a, b) => $"{a}\r\n\r\n{b}");
//System.IO.File.WriteAllText("parents.json", parentsDebug);
#endif
Serilog.Log.Logger.Debug("Beginning episode scan for {parent}", parent);
if (!parents.Any())
return;
Serilog.Log.Logger.Information($"{parents.Count} series of shows/podcasts found");
// remove episode parents. even if the following stuff fails, these will still be removed from the collection
items.RemoveAll(i => i.IsEpisodes);
if (importEpisodes)
{
// add children
var children = await getEpisodesAsync(parents);
Serilog.Log.Logger.Information($"{children.Count} episodes of shows/podcasts found");
items.AddRange(children);
}
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Error adding podcasts and episodes");
}
}
private async Task<List<Item>> getEpisodesAsync(List<Item> parents)
{
var results = new List<Item>();
foreach (var parent in parents)
{
var children = await getEpisodeChildrenAsync(parent);
// actual individual episode, not the parent of a series.
// for now I'm keeping it inside this method since it fits the work flow, incl. importEpisodes logic
if (!children.Any())
{
results.Add(parent);
continue;
}
return new List<Item>() { parent };
foreach (var child in children)
{
@@ -232,10 +206,14 @@ namespace AudibleUtilities
};
}
results.AddRange(children);
}
Serilog.Log.Logger.Debug("Completed episode scan for {parent}", parent);
return results;
return children;
}
finally
{
concurrencySemaphore.Release();
}
}
private async Task<List<Item>> getEpisodeChildrenAsync(Item parent)
@@ -277,7 +255,7 @@ namespace AudibleUtilities
throw;
}
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results");
Serilog.Log.Logger.Debug($"Batch {i}: {childrenBatch.Count} results\t({{parent}})", parent);
// the service returned no results. probably indicates an error. stop running batches
if (!childrenBatch.Any())
break;
@@ -295,7 +273,7 @@ namespace AudibleUtilities
if (childrenIds.Count != results.Count)
{
var ex = new ApplicationException($"Mis-match: Children defined by parent={childrenIds.Count}. Children returned by batches={results.Count}");
Serilog.Log.Logger.Error(ex, "Quantity of series episodes defined by parent does not match quantity returned by batch fetching.");
Serilog.Log.Logger.Error(ex, "{parent} - Quantity of series episodes defined by parent does not match quantity returned by batch fetching.", parent);
throw ex;
}

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AudibleApi" Version="2.8.1.1" />
<PackageReference Include="AudibleApi" Version="3.0.2.1" />
</ItemGroup>
<ItemGroup>

View File

@@ -12,7 +12,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.0.1.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="4.1.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -29,7 +29,7 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<None Update="migrate.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

View File

@@ -12,7 +12,7 @@ namespace DataLayer
public float StoryRating { get; private set; }
private Rating() { }
internal Rating(float overallRating, float performanceRating, float storyRating)
public Rating(float overallRating, float performanceRating, float storyRating)
{
OverallRating = overallRating;
PerformanceRating = performanceRating;

View File

@@ -1,6 +0,0 @@
{
"ConnectionStrings": {
"// this connection string is ONLY used for DataLayer's Migrations. this appsettings.json file is NOT used at all by application; it is overwritten": "",
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
}
}

View File

@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"LibationContext": "Data Source=LibationContext.db;Foreign Keys=False;"
}
}

View File

@@ -4,7 +4,8 @@ namespace FileLiberator
{
public abstract class AudioDecodable : Processable
{
public event EventHandler<Action<byte[]>> RequestCoverArt;
public delegate byte[] RequestCoverArtHandler(object sender, EventArgs eventArgs);
public event RequestCoverArtHandler RequestCoverArt;
public event EventHandler<string> TitleDiscovered;
public event EventHandler<string> AuthorsDiscovered;
public event EventHandler<string> NarratorsDiscovered;
@@ -32,10 +33,10 @@ namespace FileLiberator
NarratorsDiscovered?.Invoke(this, narrators);
}
protected void OnRequestCoverArt(Action<byte[]> setCoverArtDel)
protected byte[] OnRequestCoverArt()
{
Serilog.Log.Logger.Debug("Event fired {@DebugInfo}", new { Name = nameof(RequestCoverArt) });
RequestCoverArt?.Invoke(this, setCoverArtDel);
return RequestCoverArt?.Invoke(this, new());
}
protected void OnCoverImageDiscovered(byte[] coverImage)

View File

@@ -14,19 +14,27 @@ namespace FileLiberator
{
public class ConvertToMp3 : AudioDecodable
{
public override string Name => "Convert to Mp3";
private Mp4File m4bBook;
private long fileSize;
private long fileSize;
private static string Mp3FileName(string m4bPath) => Path.ChangeExtension(m4bPath ?? "", ".mp3");
public override void Cancel() => m4bBook?.Cancel();
private bool cancelled = false;
public override void Cancel()
{
m4bBook?.Cancel();
cancelled = true;
}
public override bool Validate(LibraryBook libraryBook)
{
public static bool ValidateMp3(LibraryBook libraryBook)
{
var path = AudibleFileStorage.Audio.GetPath(libraryBook.Book.AudibleProductId);
return path?.ToLower()?.EndsWith(".m4b") == true && !File.Exists(Mp3FileName(path));
}
public override bool Validate(LibraryBook libraryBook) => ValidateMp3(libraryBook);
public override async Task<StatusHandler> ProcessAsync(LibraryBook libraryBook)
{
OnBegin(libraryBook);
@@ -56,12 +64,12 @@ namespace FileLiberator
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path);
OnFileCreated(libraryBook, realMp3Path);
var statusHandler = new StatusHandler();
if (result == ConversionResult.Failed)
statusHandler.AddError("Conversion failed");
return statusHandler;
return new StatusHandler { "Conversion failed" };
else if (result == ConversionResult.Cancelled)
return new StatusHandler { "Cancelled" };
else
return new StatusHandler();
}
finally
{

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AaxDecrypter;
using ApplicationServices;
using AudibleApi;
using DataLayer;
using Dinah.Core;
@@ -15,6 +16,7 @@ namespace FileLiberator
{
public class DownloadDecryptBook : AudioDecodable
{
public override string Name => "Download & Decrypt";
private AudiobookDownloadBase abDownloader;
public override bool Validate(LibraryBook libraryBook) => !libraryBook.Book.Audio_Exists();
@@ -65,11 +67,14 @@ namespace FileLiberator
foreach (var tmpFile in entries.Where(f => f.FileType != FileType.AAXC))
FileUtility.SaferDelete(tmpFile.Path);
return new StatusHandler { "Decrypt failed" };
return abDownloader?.IsCanceled == true ?
new StatusHandler { "Cancelled" } :
new StatusHandler { "Decrypt failed" };
}
// moves new files from temp dir to final dest
var movedAudioFile = moveFilesToBooksDir(libraryBook, entries);
// moves new files from temp dir to final dest.
// This could take a few seconds if moving hundreds of files.
var movedAudioFile = await Task.Run(() => moveFilesToBooksDir(libraryBook, entries));
// decrypt failed
if (!movedAudioFile)
@@ -78,8 +83,7 @@ namespace FileLiberator
if (Configuration.Instance.DownloadCoverArt)
DownloadCoverArt(libraryBook);
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Liberated;
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Liberated);
return new StatusHandler();
}
@@ -243,7 +247,7 @@ namespace FileLiberator
if (e is not null)
OnCoverImageDiscovered(e);
else if (Configuration.Instance.AllowLibationFixup)
OnRequestCoverArt(abDownloader.SetCoverArt);
abDownloader.SetCoverArt(OnRequestCoverArt());
}
/// <summary>Move new files to 'Books' directory</summary>

View File

@@ -1,31 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Dinah.Core.Net.Http;
namespace FileLiberator
{
// currently only used to download the .zip flies for upgrade
public class DownloadFile : Streamable
{
public async Task<string> PerformDownloadFileAsync(string downloadUrl, string proposedDownloadFilePath)
{
var client = new HttpClient();
var progress = new Progress<DownloadProgress>(OnStreamingProgressChanged);
OnStreamingBegin(proposedDownloadFilePath);
try
{
var actualDownloadedFilePath = await client.DownloadFileAsync(downloadUrl, proposedDownloadFilePath, progress);
OnFileCreated("Upgrade", actualDownloadedFilePath);
return actualDownloadedFilePath;
}
finally
{
OnStreamingCompleted(proposedDownloadFilePath);
}
}
}
}

View File

@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using ApplicationServices;
using DataLayer;
using Dinah.Core.ErrorHandling;
using Dinah.Core.Net.Http;
@@ -14,6 +15,7 @@ namespace FileLiberator
{
public class DownloadPdf : Processable
{
public override string Name => "Download Pdf";
public override bool Validate(LibraryBook libraryBook)
=> !string.IsNullOrWhiteSpace(getdownloadUrl(libraryBook))
&& !libraryBook.Book.PDF_Exists();
@@ -28,8 +30,7 @@ namespace FileLiberator
var actualDownloadedFilePath = await downloadPdfAsync(libraryBook, proposedDownloadFilePath);
var result = verifyDownload(actualDownloadedFilePath);
libraryBook.Book.UserDefinedItem.PdfStatus = result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated;
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
libraryBook.Book.UpdatePdfStatus(result.IsSuccess ? LiberatedStatus.Liberated : LiberatedStatus.NotLiberated);
return result;
}

View File

@@ -11,6 +11,7 @@ namespace FileLiberator
{
public abstract class Processable : Streamable
{
public abstract string Name { get; }
public event EventHandler<LibraryBook> Begin;
/// <summary>General string message to display. DON'T rely on this for success, failure, or control logic</summary>

View File

@@ -5,7 +5,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="4.2.0.1" />
<PackageReference Include="Dinah.Core" Version="4.4.0.1" />
<PackageReference Include="Polly" Version="7.2.3" />
</ItemGroup>

View File

@@ -7,6 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
ProjectSection(SolutionItems) = preProject
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt
__TODO.txt = __TODO.txt
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
_DB_NOTES.txt = _DB_NOTES.txt
REFERENCE.txt = REFERENCE.txt
EndProjectSection

View File

@@ -29,11 +29,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.8.0" />
<PackageReference Include="CommandLineParser" Version="2.9.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
<ProjectReference Include="..\AppScaffolding\AppScaffolding.csproj" />
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>

View File

@@ -9,7 +9,6 @@ using FileLiberator;
namespace LibationCli
{
// streamlined, non-Forms copy of ProcessorAutomationController
public abstract class ProcessableOptionsBase : OptionsBase
{
protected static TProcessable CreateProcessable<TProcessable>(EventHandler<LibraryBook> completedAction = null)

View File

@@ -27,7 +27,6 @@ namespace LibationCli
// //
//***********************************************//
Setup.Initialize();
Setup.SubscribeToDatabaseEvents();
var types = Setup.LoadVerbs();

View File

@@ -34,15 +34,15 @@ namespace LibationCli
private static void checkForUpdate()
{
var (hasUpgrade, zipUrl, htmlUrl, zipName) = LibationScaffolding.GetLatestRelease();
if (!hasUpgrade)
var upgradeProperties = LibationScaffolding.GetLatestRelease();
if (upgradeProperties is null)
return;
var origColor = Console.ForegroundColor;
try
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"UPDATE AVAILABLE @ {zipUrl}");
Console.WriteLine($"UPDATE AVAILABLE @ {upgradeProperties.ZipUrl}");
}
finally
{
@@ -50,11 +50,6 @@ namespace LibationCli
}
}
public static void SubscribeToDatabaseEvents()
{
DataLayer.UserDefinedItem.ItemChanged += (sender, e) => ApplicationServices.LibraryCommands.UpdateUserDefinedItem(((DataLayer.UserDefinedItem)sender).Book);
}
public static Type[] LoadVerbs() => Assembly.GetExecutingAssembly()
.GetTypes()
.Where(t => t.GetCustomAttribute<VerbAttribute>() is not null)

View File

@@ -268,6 +268,13 @@ namespace LibationFileManager
}
}
[Description("Auto download episodes? Efter scan, download new books in 'checked' accounts.")]
public bool AutoDownloadEpisodes
{
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
}
#region templates: custom file naming
[Description("How to format the folders in which files will be saved")]
@@ -421,7 +428,7 @@ namespace LibationFileManager
return libationFilesPathCache;
// FIRST: must write here before SettingsFilePath in next step reads cache
libationFilesPathCache = getLiberationFilesSettingFromJson();
libationFilesPathCache = getLibationFilesSettingFromJson();
// SECOND. before setting to json file with SetWithJsonPath, PersistentDictionary must exist
persistentDictionary = new PersistentDictionary(SettingsFilePath);
@@ -443,7 +450,7 @@ namespace LibationFileManager
private static string libationFilesPathCache;
private string getLiberationFilesSettingFromJson()
private string getLibationFilesSettingFromJson()
{
string startingContents = null;
try
@@ -482,6 +489,14 @@ namespace LibationFileManager
{
libationFilesPathCache = null;
// ensure exists
if (!File.Exists(APPSETTINGS_JSON))
{
// getter creates new file, loads PersistentDictionary
var _ = LibationFiles;
System.Threading.Thread.Sleep(100);
}
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
var jObj = JObject.Parse(startingContents);

View File

@@ -6,7 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.1.0" />
<PackageReference Include="Serilog.Exceptions" Version="8.2.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -28,16 +28,22 @@ namespace LibationFileManager
inMemoryState = JsonConvert.DeserializeObject<FilterState>(File.ReadAllText(JsonFile));
}
public static event EventHandler UseDefaultChanged;
public static bool UseDefault
{
get => inMemoryState.UseDefault;
set
{
if (UseDefault == value)
return;
lock (locker)
{
inMemoryState.UseDefault = value;
save(false);
}
UseDefaultChanged?.Invoke(null, null);
}
}

View File

@@ -18,24 +18,18 @@ namespace LibationSearchEngine
{
public const Lucene.Net.Util.Version Version = Lucene.Net.Util.Version.LUCENE_30;
// not customizable. don't move to config
private static string SearchEngineDirectory { get; }
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
public const string _ID_ = "_ID_";
public const string TAGS = "tags";
// special field for each book which includes all major parts of the book's metadata. enables non-targetting searching
public const string ALL = "all";
// the workaround which allows displaying all books when query is empty
public const string ALL_QUERY = "*:*";
#region index rules
// common fields used in the "all" default search field
public const string ALL_AUDIBLE_PRODUCT_ID = nameof(Book.AudibleProductId);
public const string ALL_TITLE = nameof(Book.Title);
public const string ALL_AUTHOR_NAMES = "AuthorNames";
public const string ALL_NARRATOR_NAMES = "NarratorNames";
public const string ALL_SERIES_NAMES = "SeriesNames";
private static ReadOnlyDictionary<string, Func<LibraryBook, string>> idIndexRules { get; }
= new ReadOnlyDictionary<string, Func<LibraryBook, string>>(
@@ -64,16 +58,8 @@ namespace LibationSearchEngine
["Narrators"] = lb => lb.Book.NarratorNames(),
[nameof(Book.Publisher)] = lb => lb.Book.Publisher,
["SeriesNames"] = lb => string.Join(
", ",
lb.Book.SeriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)),
["Series"] = lb => string.Join(
", ",
lb.Book.SeriesLink
.Where(s => !string.IsNullOrWhiteSpace(s.Series.Name))
.Select(s => s.Series.AudibleSeriesId)),
[ALL_SERIES_NAMES] = lb => lb.Book.SeriesNames(),
["Series"] = lb => lb.Book.SeriesNames(),
["SeriesId"] = lb => string.Join(", ", lb.Book.SeriesLink.Select(s => s.Series.AudibleSeriesId)),
["CategoriesNames"] = lb => lb.Book.CategoriesIds() is null ? null : string.Join(", ", lb.Book.CategoriesIds()),
@@ -131,14 +117,15 @@ namespace LibationSearchEngine
[nameof(Book.IsAbridged)] = lb => lb.Book.IsAbridged,
["Abridged"] = lb => lb.Book.IsAbridged,
// this will only be evaluated at time of re-index. ie: state of files moved later will be out of sync until next re-index
["IsLiberated"] = lb => isLiberated(lb.Book),
["Liberated"] = lb => isLiberated(lb.Book),
["LiberatedError"] = lb => liberatedError(lb.Book),
["Podcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["Podcasts"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsPodcast"] = lb => lb.Book.ContentType == ContentType.Episode,
["Episode"] = lb => lb.Book.ContentType == ContentType.Episode,
["Episodes"] = lb => lb.Book.ContentType == ContentType.Episode,
["IsEpisode"] = lb => lb.Book.ContentType == ContentType.Episode,
}
);
@@ -160,7 +147,8 @@ namespace LibationSearchEngine
idIndexRules[ALL_AUDIBLE_PRODUCT_ID],
stringIndexRules[ALL_TITLE],
stringIndexRules[ALL_AUTHOR_NAMES],
stringIndexRules[ALL_NARRATOR_NAMES]
stringIndexRules[ALL_NARRATOR_NAMES],
stringIndexRules[ALL_SERIES_NAMES]
};
#endregion
@@ -188,18 +176,6 @@ namespace LibationSearchEngine
foreach (var key in numberIndexRules.Keys)
yield return key;
}
public static IEnumerable<string> GetSearchFields()
{
foreach (var key in idIndexRules.Keys)
yield return key;
foreach (var key in stringIndexRules.Keys)
yield return key;
foreach (var key in boolIndexRules.Keys)
yield return key;
foreach (var key in numberIndexRules.Keys)
yield return key;
}
#endregion
#region create and update index
@@ -296,6 +272,10 @@ namespace LibationSearchEngine
book.AudibleProductId,
d =>
{
//
// TODO: better synonym handling. This is too easy to mess up
//
// fields are key value pairs. MULTIPLE FIELDS CAN POTENTIALLY HAVE THE SAME KEY.
// ie: must remove old before adding new else will create unwanted duplicates.
var v1 = isLiberated(book);
@@ -337,6 +317,9 @@ namespace LibationSearchEngine
}
#endregion
// the workaround which allows displaying all books when query is empty
public const string ALL_QUERY = "*:*";
#region search
public SearchResultSet Search(string searchString)
{
@@ -351,7 +334,7 @@ namespace LibationSearchEngine
return results;
}
public static string FormatSearchQuery(string searchString)
internal static string FormatSearchQuery(string searchString)
{
if (string.IsNullOrWhiteSpace(searchString))
return ALL_QUERY;
@@ -497,5 +480,9 @@ namespace LibationSearchEngine
#endregion
private static Directory getIndex() => FSDirectory.Open(SearchEngineDirectory);
// not customizable. don't move to config
private static string SearchEngineDirectory { get; }
= new System.IO.DirectoryInfo(Configuration.Instance.LibationFiles).CreateSubdirectory("SearchEngine").FullName;
}
}

View File

@@ -1,34 +0,0 @@
using System;
using DataLayer;
using LibationFileManager;
namespace LibationWinForms.BookLiberation
{
class AudioConvertForm : AudioDecodeForm
{
public AudioConvertForm()
{
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
#region AudioDecodeForm overrides
public override string DecodeActionName => "Converting";
#endregion
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
LogMe.Info($"Convert Step, Begin: {libraryBook.Book}");
base.Processable_Begin(sender, libraryBook);
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"Convert Step, Completed: {libraryBook.Book}{Environment.NewLine}");
}
#endregion
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,104 +0,0 @@
namespace LibationWinForms.BookLiberation
{
partial class AudioDecodeForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.bookInfoLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.remainingTimeLbl = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.Location = new System.Drawing.Point(14, 14);
this.pictureBox1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(117, 115);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
// bookInfoLbl
//
this.bookInfoLbl.AutoSize = true;
this.bookInfoLbl.Location = new System.Drawing.Point(138, 14);
this.bookInfoLbl.Margin = new System.Windows.Forms.Padding(4, 0, 4, 0);
this.bookInfoLbl.Name = "bookInfoLbl";
this.bookInfoLbl.Size = new System.Drawing.Size(121, 15);
this.bookInfoLbl.TabIndex = 0;
this.bookInfoLbl.Text = "[multi-line book info]";
//
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(14, 143);
this.progressBar1.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(611, 27);
this.progressBar1.TabIndex = 2;
//
// remainingTimeLbl
//
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.remainingTimeLbl.Location = new System.Drawing.Point(632, 143);
this.remainingTimeLbl.Name = "remainingTimeLbl";
this.remainingTimeLbl.Size = new System.Drawing.Size(60, 31);
this.remainingTimeLbl.TabIndex = 3;
this.remainingTimeLbl.Text = "ETA:\r\n";
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// DecryptForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(707, 183);
this.Controls.Add(this.remainingTimeLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.bookInfoLbl);
this.Controls.Add(this.pictureBox1);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "DecryptForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "DecryptForm";
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.Label bookInfoLbl;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label remainingTimeLbl;
}
}

View File

@@ -1,114 +0,0 @@
using System;
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using LibationFileManager;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
public partial class AudioDecodeForm : LiberationBaseForm
{
public virtual string DecodeActionName { get; } = "Decoding";
public AudioDecodeForm() => InitializeComponent();
private Func<byte[]> GetCoverArtDelegate;
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
base.Processable_Begin(sender, libraryBook);
GetCoverArtDelegate = () => PictureStorage.GetPictureSynchronously(
new PictureDefinition(
libraryBook.Book.PictureId,
PictureSize._500x500));
//Set default values from library
AudioDecodable_TitleDiscovered(sender, libraryBook.Book.Title);
AudioDecodable_AuthorsDiscovered(sender, libraryBook.Book.AuthorNames());
AudioDecodable_NarratorsDiscovered(sender, libraryBook.Book.NarratorNames());
AudioDecodable_CoverImageDiscovered(sender,
PictureStorage.GetPicture(
new PictureDefinition(
libraryBook.Book.PictureId,
PictureSize._80x80)).bytes);
}
#endregion
#region Streamable event handler overrides
public override void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
base.Streamable_StreamingProgressChanged(sender, downloadProgress);
if (!downloadProgress.ProgressPercentage.HasValue)
return;
if (downloadProgress.ProgressPercentage == 0)
updateRemainingTime(0);
else
progressBar1.UIThreadAsync(() => progressBar1.Value = (int)downloadProgress.ProgressPercentage);
}
public override void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
{
base.Streamable_StreamingTimeRemaining(sender, timeRemaining);
updateRemainingTime((int)timeRemaining.TotalSeconds);
}
private void updateRemainingTime(int remaining)
=> remainingTimeLbl.UIThreadAsync(() => remainingTimeLbl.Text = $"ETA:\r\n{formatTime(remaining)}");
private string formatTime(int seconds)
{
var timeSpan = new TimeSpan(0, 0, seconds);
return
timeSpan.TotalHours >= 1 ? $"{timeSpan:%h}h {timeSpan:mm}m {timeSpan:ss}s"
: timeSpan.TotalMinutes >= 1 ? $"{timeSpan:%m}m {timeSpan:ss}s"
: $"{seconds} sec";
}
#endregion
#region AudioDecodable event handlers
private string title;
private string authorNames;
private string narratorNames;
public override void AudioDecodable_TitleDiscovered(object sender, string title)
{
base.AudioDecodable_TitleDiscovered(sender, title);
this.UIThreadAsync(() => this.Text = DecodeActionName + " " + title);
this.title = title;
updateBookInfo();
}
public override void AudioDecodable_AuthorsDiscovered(object sender, string authors)
{
base.AudioDecodable_AuthorsDiscovered(sender, authors);
authorNames = authors;
updateBookInfo();
}
public override void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
{
base.AudioDecodable_NarratorsDiscovered(sender, narrators);
narratorNames = narrators;
updateBookInfo();
}
private void updateBookInfo()
=> bookInfoLbl.UIThreadAsync(() => bookInfoLbl.Text = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}");
public override void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate)
{
base.AudioDecodable_RequestCoverArt(sender, setCoverArtDelegate);
setCoverArtDelegate(GetCoverArtDelegate?.Invoke());
}
public override void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
{
base.AudioDecodable_CoverImageDiscovered(sender, coverArt);
pictureBox1.UIThreadAsync(() => pictureBox1.Image = Dinah.Core.Drawing.ImageReader.ToImage(coverArt));
}
#endregion
}
}

View File

@@ -1,34 +0,0 @@
using System;
using DataLayer;
using LibationFileManager;
namespace LibationWinForms.BookLiberation
{
class AudioDecryptForm : AudioDecodeForm
{
public AudioDecryptForm()
{
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
}
#region AudioDecodeForm overrides
public override string DecodeActionName => "Decrypting";
#endregion
#region Processable event handler overrides
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
LogMe.Info($"Download & Decrypt Step, Begin: {libraryBook.Book}");
base.Processable_Begin(sender, libraryBook);
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"Download & Decrypt Step, Completed: {libraryBook.Book}{Environment.NewLine}");
}
#endregion
}
}

View File

@@ -1,93 +0,0 @@
namespace LibationWinForms.BookLiberation
{
partial class AutomatedBackupsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.keepGoingCb = new System.Windows.Forms.CheckBox();
this.logTb = new System.Windows.Forms.TextBox();
this.label1 = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// keepGoingCb
//
this.keepGoingCb.AutoSize = true;
this.keepGoingCb.Checked = true;
this.keepGoingCb.CheckState = System.Windows.Forms.CheckState.Checked;
this.keepGoingCb.Location = new System.Drawing.Point(12, 12);
this.keepGoingCb.Name = "keepGoingCb";
this.keepGoingCb.Size = new System.Drawing.Size(325, 17);
this.keepGoingCb.TabIndex = 0;
this.keepGoingCb.Text = "Keep going. Uncheck to stop when current backup is complete";
this.keepGoingCb.UseVisualStyleBackColor = true;
//
// logTb
//
this.logTb.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.logTb.Location = new System.Drawing.Point(12, 48);
this.logTb.Multiline = true;
this.logTb.Name = "logTb";
this.logTb.ReadOnly = true;
this.logTb.ScrollBars = System.Windows.Forms.ScrollBars.Both;
this.logTb.Size = new System.Drawing.Size(960, 202);
this.logTb.TabIndex = 1;
//
// label1
//
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(9, 32);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(501, 13);
this.label1.TabIndex = 2;
this.label1.Text = "NOTE: if the working directories are inside of Dropbox, some book liberation acti" +
"ons may hang indefinitely";
//
// AutomatedBackupsForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(984, 262);
this.Controls.Add(this.label1);
this.Controls.Add(this.logTb);
this.Controls.Add(this.keepGoingCb);
this.Name = "AutomatedBackupsForm";
this.Text = "Automated Backups";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.AutomatedBackupsForm_FormClosing);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.CheckBox keepGoingCb;
private System.Windows.Forms.TextBox logTb;
private System.Windows.Forms.Label label1;
}
}

View File

@@ -1,34 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Threading;
namespace LibationWinForms.BookLiberation
{
public partial class AutomatedBackupsForm : Form
{
public bool KeepGoingChecked => keepGoingCb.Checked;
public bool KeepGoing => keepGoingCb.Enabled && keepGoingCb.Checked;
public AutomatedBackupsForm()
{
InitializeComponent();
}
public void WriteLine(string text)
{
if (!IsDisposed)
logTb.UIThreadAsync(() => logTb.AppendText($"{DateTime.Now} {text}{Environment.NewLine}"));
}
public void FinalizeUI()
{
keepGoingCb.Enabled = false;
if (!IsDisposed)
logTb.AppendText("");
}
private void AutomatedBackupsForm_FormClosing(object sender, FormClosingEventArgs e) => keepGoingCb.Checked = false;
}
}

View File

@@ -1,164 +0,0 @@
using System;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using FileLiberator;
namespace LibationWinForms.BookLiberation.BaseForms
{
public class LiberationBaseForm : Form
{
protected Streamable Streamable { get; private set; }
protected LogMe LogMe { get; private set; }
private SynchronizeInvoker Invoker { get; init; }
public LiberationBaseForm()
{
//SynchronizationContext.Current will be null until the process contains a Form.
//If this is the first form created, it will not exist until after execution
//reaches inside the constructor (after base class has been initialized).
Invoker = new SynchronizeInvoker();
this.SetLibationIcon();
}
public void RegisterFileLiberator(Streamable streamable, LogMe logMe = null)
{
if (streamable is null) return;
Streamable = streamable;
LogMe = logMe;
Subscribe(streamable);
if (Streamable is Processable processable)
Subscribe(processable);
if (Streamable is AudioDecodable audioDecodable)
Subscribe(audioDecodable);
}
#region Event Subscribers and Unsubscribers
private void Subscribe(Streamable streamable)
{
UnsubscribeStreamable(this, EventArgs.Empty);
streamable.StreamingBegin += OnStreamingBeginShow;
streamable.StreamingBegin += Streamable_StreamingBegin;
streamable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
streamable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
streamable.StreamingCompleted += Streamable_StreamingCompleted;
streamable.StreamingCompleted += OnStreamingCompletedClose;
Disposed += UnsubscribeStreamable;
}
private void Subscribe(Processable processable)
{
UnsubscribeProcessable(this, null);
processable.Begin += Processable_Begin;
processable.StatusUpdate += Processable_StatusUpdate;
processable.Completed += Processable_Completed;
//The form is created on Processable.Begin and we
//dispose of it on Processable.Completed
processable.Completed += OnCompletedDispose;
//Don't unsubscribe from Dispose because it fires when
//Streamable.StreamingCompleted closes the form, and
//the Processable events need to live past that event.
processable.Completed += UnsubscribeProcessable;
}
private void Subscribe(AudioDecodable audioDecodable)
{
UnsubscribeAudioDecodable(this, EventArgs.Empty);
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
Disposed += UnsubscribeAudioDecodable;
}
private void UnsubscribeStreamable(object sender, EventArgs e)
{
Disposed -= UnsubscribeStreamable;
Streamable.StreamingBegin -= OnStreamingBeginShow;
Streamable.StreamingBegin -= Streamable_StreamingBegin;
Streamable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
Streamable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
Streamable.StreamingCompleted -= Streamable_StreamingCompleted;
Streamable.StreamingCompleted -= OnStreamingCompletedClose;
}
private void UnsubscribeProcessable(object sender, LibraryBook e)
{
if (Streamable is not Processable processable)
return;
processable.Completed -= UnsubscribeProcessable;
processable.Completed -= OnCompletedDispose;
processable.Completed -= Processable_Completed;
processable.StatusUpdate -= Processable_StatusUpdate;
processable.Begin -= Processable_Begin;
}
private void UnsubscribeAudioDecodable(object sender, EventArgs e)
{
if (Streamable is not AudioDecodable audioDecodable)
return;
Disposed -= UnsubscribeAudioDecodable;
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
audioDecodable.Cancel();
}
#endregion
#region Form creation and disposal handling
/// <summary>
/// If the form was shown using Show (not ShowDialog), Form.Close calls Form.Dispose
/// </summary>
private void OnStreamingCompletedClose(object sender, string completedString) => this.UIThreadAsync(Close);
private void OnCompletedDispose(object sender, LibraryBook e) => this.UIThreadAsync(Dispose);
/// <summary>
/// If StreamingBegin is fired from a worker thread, the window will be created on that
/// worker thread. We need to make certain that we show the window on the UI thread (same
/// thread that created form), otherwise the renderer will be on a worker thread which
/// could cause it to freeze. Form.BeginInvoke won't work until the form is created
/// (ie. shown) because Control doesn't get a window handle until it is Shown.
/// </summary>
private void OnStreamingBeginShow(object sender, string beginString) => Invoker.UIThreadAsync(Show);
#endregion
#region Streamable event handlers
public virtual void Streamable_StreamingBegin(object sender, string beginString) { }
public virtual void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress) { }
public virtual void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining) { }
public virtual void Streamable_StreamingCompleted(object sender, string completedString) { }
#endregion
#region Processable event handlers
public virtual void Processable_Begin(object sender, LibraryBook libraryBook) { }
public virtual void Processable_StatusUpdate(object sender, string statusUpdate) { }
public virtual void Processable_Completed(object sender, LibraryBook libraryBook) { }
#endregion
#region AudioDecodable event handlers
public virtual void AudioDecodable_TitleDiscovered(object sender, string title) { }
public virtual void AudioDecodable_AuthorsDiscovered(object sender, string authors) { }
public virtual void AudioDecodable_NarratorsDiscovered(object sender, string narrators) { }
public virtual void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt) { }
public virtual void AudioDecodable_RequestCoverArt(object sender, Action<byte[]> setCoverArtDelegate) { }
#endregion
}
}

View File

@@ -1,109 +0,0 @@
using DataLayer;
using System;
namespace LibationWinForms.BookLiberation
{
partial class DownloadForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.filenameLbl = new System.Windows.Forms.Label();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.progressLbl = new System.Windows.Forms.Label();
this.lastUpdateLbl = new System.Windows.Forms.Label();
this.SuspendLayout();
//
// filenameLbl
//
this.filenameLbl.AutoSize = true;
this.filenameLbl.Location = new System.Drawing.Point(12, 9);
this.filenameLbl.Name = "filenameLbl";
this.filenameLbl.Size = new System.Drawing.Size(52, 13);
this.filenameLbl.TabIndex = 0;
this.filenameLbl.Text = "[filename]";
//
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(15, 67);
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(877, 23);
this.progressBar1.TabIndex = 4;
//
// progressLbl
//
this.progressLbl.Location = new System.Drawing.Point(12, 36);
this.progressLbl.Name = "progressLbl";
this.progressLbl.Size = new System.Drawing.Size(173, 13);
this.progressLbl.TabIndex = 5;
this.progressLbl.Text = "[2,999,999,999] of [2,999,999,999]";
this.progressLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// lastUpdateLbl
//
this.lastUpdateLbl.AutoSize = true;
this.lastUpdateLbl.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.lastUpdateLbl.ForeColor = System.Drawing.Color.DarkRed;
this.lastUpdateLbl.Location = new System.Drawing.Point(361, 36);
this.lastUpdateLbl.Name = "lastUpdateLbl";
this.lastUpdateLbl.Size = new System.Drawing.Size(81, 13);
this.lastUpdateLbl.TabIndex = 6;
this.lastUpdateLbl.Text = "Last updated";
this.lastUpdateLbl.Visible = false;
//
// DownloadForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(904, 102);
this.Controls.Add(this.lastUpdateLbl);
this.Controls.Add(this.progressLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.filenameLbl);
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "DownloadForm";
this.ShowIcon = false;
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "Downloading";
this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.DownloadForm_FormClosing);
this.Load += new System.EventHandler(this.DownloadForm_Load);
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label filenameLbl;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label progressLbl;
private System.Windows.Forms.Label lastUpdateLbl;
}
}

View File

@@ -1,67 +0,0 @@
using System;
using System.Windows.Forms;
using Dinah.Core.Net.Http;
using Dinah.Core.Threading;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
public partial class DownloadForm : LiberationBaseForm
{
public DownloadForm()
{
InitializeComponent();
progressLbl.Text = "";
filenameLbl.Text = "";
}
#region Streamable event handler overrides
public override void Streamable_StreamingBegin(object sender, string beginString)
{
base.Streamable_StreamingBegin(sender, beginString);
filenameLbl.UIThreadAsync(() => filenameLbl.Text = beginString);
}
public override void Streamable_StreamingProgressChanged(object sender, DownloadProgress downloadProgress)
{
base.Streamable_StreamingProgressChanged(sender, downloadProgress);
// this won't happen with download file. it will happen with download string
if (!downloadProgress.TotalBytesToReceive.HasValue || downloadProgress.TotalBytesToReceive.Value <= 0)
return;
progressLbl.UIThreadAsync(() => progressLbl.Text = $"{downloadProgress.BytesReceived:#,##0} of {downloadProgress.TotalBytesToReceive.Value:#,##0}");
var d = double.Parse(downloadProgress.BytesReceived.ToString()) / double.Parse(downloadProgress.TotalBytesToReceive.Value.ToString()) * 100.0;
var i = int.Parse(Math.Truncate(d).ToString());
progressBar1.UIThreadAsync(() => progressBar1.Value = i);
lastDownloadProgress = DateTime.Now;
}
#endregion
#region timer
private Timer timer { get; } = new Timer { Interval = 1000 };
private void DownloadForm_Load(object sender, EventArgs e)
{
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
private DateTime lastDownloadProgress = DateTime.Now;
private void timer_Tick(object sender, EventArgs e)
{
// if no update in the last 30 seconds, display frozen label
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Visible = lastDownloadProgress.AddSeconds(30) < DateTime.Now);
if (lastUpdateLbl.Visible)
{
var diff = DateTime.Now - lastDownloadProgress;
var min = (int)diff.TotalMinutes;
var minText = min > 0 ? $"{min}min " : "";
lastUpdateLbl.UIThreadAsync(() => lastUpdateLbl.Text = $"Frozen? Last download activity: {minText}{diff.Seconds}sec ago");
}
}
private void DownloadForm_FormClosing(object sender, FormClosingEventArgs e) => timer.Stop();
#endregion
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,18 +0,0 @@
using DataLayer;
namespace LibationWinForms.BookLiberation
{
internal class PdfDownloadForm : DownloadForm
{
public override void Processable_Begin(object sender, LibraryBook libraryBook)
{
base.Processable_Begin(sender, libraryBook);
LogMe.Info($"PDF Step, Begin: {libraryBook.Book}");
}
public override void Processable_Completed(object sender, LibraryBook libraryBook)
{
base.Processable_Completed(sender, libraryBook);
LogMe.Info($"PDF Step, Completed: {libraryBook.Book}");
}
}
}

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@@ -1,343 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.BookLiberation.BaseForms;
namespace LibationWinForms.BookLiberation
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
private LogMe()
{
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
}
public static LogMe RegisterForm(AutomatedBackupsForm form = null)
{
var logMe = new LogMe();
if (form is null)
return logMe;
logMe.LogInfo += (_, text) => form?.WriteLine(text);
logMe.LogErrorString += (_, text) => form?.WriteLine(text);
logMe.LogError += (_, tuple) =>
{
form?.WriteLine(tuple.Item2 ?? "Automated backup: error");
form?.WriteLine("ERROR: " + tuple.Item1.Message);
};
return logMe;
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
public static class ProcessorAutomationController
{
public static async Task BackupSingleBookAsync(LibraryBook libraryBook)
{
Serilog.Log.Logger.Information($"Begin {nameof(BackupSingleBookAsync)} {{@DebugInfo}}", new { libraryBook?.Book?.AudibleProductId });
var logMe = LogMe.RegisterForm();
var backupBook = CreateBackupBook(logMe);
// continue even if libraryBook is null. we'll display even that in the processing box
await new BackupSingle(logMe, backupBook, libraryBook).RunBackupAsync();
}
public static async Task BackupAllBooksAsync(List<LibraryBook> libraryBooks = null)
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var backupBook = CreateBackupBook(logMe);
await new BackupLoop(logMe, backupBook, automatedBackupsForm, libraryBooks).RunBackupAsync();
}
public static async Task ConvertAllBooksAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(ConvertAllBooksAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var convertBook = CreateProcessable<ConvertToMp3, AudioConvertForm>(logMe);
await new BackupLoop(logMe, convertBook, automatedBackupsForm).RunBackupAsync();
}
public static async Task BackupAllPdfsAsync()
{
Serilog.Log.Logger.Information("Begin " + nameof(BackupAllPdfsAsync));
var automatedBackupsForm = new AutomatedBackupsForm();
var logMe = LogMe.RegisterForm(automatedBackupsForm);
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
await new BackupLoop(logMe, downloadPdf, automatedBackupsForm).RunBackupAsync();
}
private static Processable CreateBackupBook(LogMe logMe)
{
var downloadPdf = CreateProcessable<DownloadPdf, PdfDownloadForm>(logMe);
//Chain pdf download on DownloadDecryptBook.Completed
async void onDownloadDecryptBookCompleted(object sender, LibraryBook e)
{
await downloadPdf.TryProcessAsync(e);
}
var downloadDecryptBook = CreateProcessable<DownloadDecryptBook, AudioDecryptForm>(logMe, onDownloadDecryptBookCompleted);
return downloadDecryptBook;
}
public static void DownloadFile(string url, string destination, bool showDownloadCompletedDialog = false)
{
Serilog.Log.Logger.Information($"Begin {nameof(DownloadFile)} for {url}");
void onDownloadFileStreamingCompleted(object sender, string savedFile)
{
Serilog.Log.Logger.Information($"Completed {nameof(DownloadFile)} for {url}. Saved to {savedFile}");
if (showDownloadCompletedDialog)
MessageBox.Show($"File downloaded to:{Environment.NewLine}{Environment.NewLine}{savedFile}");
}
var downloadFile = new DownloadFile();
var downloadForm = new DownloadForm();
downloadForm.RegisterFileLiberator(downloadFile);
downloadFile.StreamingCompleted += onDownloadFileStreamingCompleted;
async void runDownload() => await downloadFile.PerformDownloadFileAsync(url, destination);
new Task(runDownload).Start();
}
/// <summary>
/// Create a new <see cref="Processable"/> and links it to a new <see cref="LiberationBaseForm"/>.
/// </summary>
/// <typeparam name="TProcessable">The <see cref="Processable"/> derived type to create.</typeparam>
/// <typeparam name="TForm">The <see cref="LiberationBaseForm"/> derived Form to create on <see cref="Processable.Begin"/>, Show on <see cref="Streamable.StreamingBegin"/>, Close on <see cref="Streamable.StreamingCompleted"/>, and Dispose on <see cref="Processable.Completed"/> </typeparam>
/// <param name="logMe">The logger</param>
/// <param name="completedAction">An additional event handler to handle <see cref="Processable.Completed"/></param>
/// <returns>A new <see cref="Processable"/> of type <typeparamref name="TProcessable"/></returns>
private static TProcessable CreateProcessable<TProcessable, TForm>(LogMe logMe, EventHandler<LibraryBook> completedAction = null)
where TForm : LiberationBaseForm, new()
where TProcessable : Processable, new()
{
var strProc = new TProcessable();
strProc.Begin += (sender, libraryBook) =>
{
var processForm = new TForm();
processForm.RegisterFileLiberator(strProc, logMe);
processForm.Processable_Begin(sender, libraryBook);
};
strProc.Completed += completedAction;
return strProc;
}
}
internal abstract class BackupRunner
{
protected LogMe LogMe { get; }
protected Processable Processable { get; }
protected AutomatedBackupsForm AutomatedBackupsForm { get; }
protected BackupRunner(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm = null)
{
LogMe = logMe;
Processable = processable;
AutomatedBackupsForm = automatedBackupsForm;
}
protected abstract Task RunAsync();
protected abstract string SkipDialogText { get; }
protected abstract MessageBoxButtons SkipDialogButtons { get; }
protected abstract MessageBoxDefaultButton SkipDialogDefaultButton { get; }
protected abstract DialogResult SkipResult { get; }
public async Task RunBackupAsync()
{
AutomatedBackupsForm?.Show();
try
{
await RunAsync();
}
catch (Exception ex)
{
LogMe.Error(ex);
}
AutomatedBackupsForm?.FinalizeUI();
LogMe.Info("DONE");
}
protected async Task<bool> ProcessOneAsync(LibraryBook libraryBook, bool validate)
{
try
{
var statusHandler = await Processable.ProcessSingleAsync(libraryBook, validate);
if (statusHandler.IsSuccess)
return true;
foreach (var errorMessage in statusHandler.Errors)
LogMe.Error(errorMessage);
}
catch (Exception ex)
{
LogMe.Error(ex);
}
return showRetry(libraryBook);
}
private bool showRetry(LibraryBook libraryBook)
{
LogMe.Error("ERROR. All books have not been processed. Most recent book: processing failed");
DialogResult? dialogResult = Configuration.Instance.BadBook switch
{
Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry,
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
Configuration.BadBookAction.Ask => null,
_ => null
};
string details;
try
{
static string trunc(string str)
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
: (str.Length > 50) ? $"{str.Truncate(47)}..."
: str;
details =
$@" Title: {libraryBook.Book.Title}
ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames())}
Narr: {trunc(libraryBook.Book.NarratorNames())}";
}
catch
{
details = "[Error retrieving details]";
}
// if null then ask user
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == DialogResult.Abort)
return false;
if (dialogResult == SkipResult)
{
libraryBook.Book.UserDefinedItem.BookStatus = LiberatedStatus.Error;
ApplicationServices.LibraryCommands.UpdateUserDefinedItem(libraryBook.Book);
LogMe.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
}
return true;
}
}
internal class BackupSingle : BackupRunner
{
private LibraryBook _libraryBook { get; }
protected override string SkipDialogText => @"
An error occurred while trying to process this book. Skip this book permanently?
{0}
- Click YES to skip this book permanently.
- Click NO to skip the book this time only. We'll try again later.
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.YesNo;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button2;
protected override DialogResult SkipResult => DialogResult.Yes;
public BackupSingle(LogMe logMe, Processable processable, LibraryBook libraryBook)
: base(logMe, processable)
{
_libraryBook = libraryBook;
}
protected override async Task RunAsync()
{
if (_libraryBook is not null)
await ProcessOneAsync(_libraryBook, validate: true);
}
}
internal class BackupLoop : BackupRunner
{
protected override string SkipDialogText => @"
An error occurred while trying to process this book.
{0}
- ABORT: Stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
protected override MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
protected override MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
protected override DialogResult SkipResult => DialogResult.Ignore;
private List<LibraryBook> libraryBooks { get; }
public BackupLoop(LogMe logMe, Processable processable, AutomatedBackupsForm automatedBackupsForm, List<LibraryBook> libraryBooks = null)
: base(logMe, processable, automatedBackupsForm)
=> this.libraryBooks = libraryBooks ?? ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking();
protected override async Task RunAsync()
{
// support for 'skip this time only' requires state. iterators provide this state for free. therefore: use foreach/iterator here
foreach (var libraryBook in Processable.GetValidLibraryBooks(libraryBooks))
{
var keepGoing = await ProcessOneAsync(libraryBook, validate: false);
if (!keepGoing)
return;
if (AutomatedBackupsForm.IsDisposed)
break;
if (!AutomatedBackupsForm.KeepGoing)
{
if (!AutomatedBackupsForm.KeepGoingChecked)
LogMe.Info("'Keep going' is unchecked");
return;
}
}
LogMe.Info("Done. All books have been processed");
}
}
}

View File

@@ -15,12 +15,8 @@ namespace LibationWinForms.Dialogs
private const string COL_AccountName = nameof(AccountName);
private const string COL_Locale = nameof(Locale);
private Form1 _parent { get; }
public AccountsDialog(Form1 parent)
public AccountsDialog()
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_AccountName].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

View File

@@ -15,12 +15,8 @@ namespace LibationWinForms.Dialogs
private const string COL_MoveUp = nameof(MoveUp);
private const string COL_MoveDown = nameof(MoveDown);
private Form1 _parent { get; }
public EditQuickFilters(Form1 parent)
public EditQuickFilters()
{
_parent = parent;
InitializeComponent();
dataGridView1.Columns[COL_Filter].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

View File

@@ -38,7 +38,7 @@ namespace LibationWinForms.Dialogs
this.authorsDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.miscDataGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.purchaseDateGridViewTextBoxColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
this.gridEntryBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
this.btnRemoveBooks = new System.Windows.Forms.Button();
this.label1 = new System.Windows.Forms.Label();
((System.ComponentModel.ISupportInitialize)(this._dataGridView)).BeginInit();
@@ -176,7 +176,7 @@ namespace LibationWinForms.Dialogs
#endregion
private System.Windows.Forms.DataGridView _dataGridView;
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
private LibationWinForms.GridView.SyncBindingSource gridEntryBindingSource;
private System.Windows.Forms.Button btnRemoveBooks;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.DataGridViewCheckBoxColumn removeDataGridViewCheckBoxColumn;

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
@@ -121,10 +120,8 @@ namespace LibationWinForms.Dialogs
}
}
internal class RemovableGridEntry : GridEntry
internal class RemovableGridEntry : GridView.LibraryBookEntry
{
private static readonly IComparer BoolComparer = new ObjectComparer<bool>();
private bool _remove = false;
public RemovableGridEntry(LibraryBook libraryBook) : base(libraryBook) { }
@@ -136,11 +133,8 @@ namespace LibationWinForms.Dialogs
}
set
{
if (_remove != value)
{
_remove = value;
NotifyPropertyChanged();
}
_remove = value;
NotifyPropertyChanged();
}
}
@@ -150,12 +144,5 @@ namespace LibationWinForms.Dialogs
return Remove;
return base.GetMemberValue(memberName);
}
public override IComparer GetMemberComparer(Type memberType)
{
if (memberType == typeof(bool))
return BoolComparer;
return base.GetMemberComparer(memberType);
}
}
}

View File

@@ -10,12 +10,8 @@ namespace LibationWinForms.Dialogs
{
public List<Account> CheckedAccounts { get; } = new List<Account>();
private Form1 _parent { get; }
public ScanAccountsDialog(Form1 parent)
public ScanAccountsDialog()
{
_parent = parent;
InitializeComponent();
this.SetLibationIcon();
}
@@ -44,7 +40,7 @@ namespace LibationWinForms.Dialogs
private void editBtn_Click(object sender, EventArgs e)
{
if (new AccountsDialog(_parent).ShowDialog() == DialogResult.OK)
if (new AccountsDialog().ShowDialog() == DialogResult.OK)
{
// clear grid
this.accountsClb.Items.Clear();

View File

@@ -21,7 +21,6 @@ namespace LibationWinForms.Dialogs
private void convertFormatRb_CheckedChanged(object sender, EventArgs e)
{
lameOptionsGb.Enabled = convertLossyRb.Checked;
lameTargetRb_CheckedChanged(sender, e);
LameMatchSourceBRCbox_CheckedChanged(sender, e);
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@ namespace LibationWinForms.Dialogs
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
@@ -80,6 +81,7 @@ namespace LibationWinForms.Dialogs
showImportedStatsCb.Checked = config.ShowImportedStats;
importEpisodesCb.Checked = config.ImportEpisodes;
downloadEpisodesCb.Checked = config.DownloadEpisodes;
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
lameTargetRb_CheckedChanged(this, e);
LameMatchSourceBRCbox_CheckedChanged(this, e);
@@ -204,6 +206,7 @@ namespace LibationWinForms.Dialogs
config.ShowImportedStats = showImportedStatsCb.Checked;
config.ImportEpisodes = importEpisodesCb.Checked;
config.DownloadEpisodes = downloadEpisodesCb.Checked;
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
config.InProgress = inProgressSelectControl.SelectedDirectory;

View File

@@ -0,0 +1,119 @@
using ApplicationServices;
using Dinah.Core;
using Dinah.Core.Threading;
namespace LibationWinForms
{
public partial class Form1
{
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
protected void Configure_BackupCounts()
{
// init formattable
beginBookBackupsToolStripMenuItem.Format(0);
beginPdfBackupsToolStripMenuItem.Format(0);
pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
Load += setBackupCounts;
LibraryCommands.LibrarySizeChanged += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
updateCountsBw.RunWorkerCompleted += updateBottomBookNumbers;
updateCountsBw.RunWorkerCompleted += update_BeginBookBackups_menuItem;
updateCountsBw.RunWorkerCompleted += updateBottomPdfNumbers;
updateCountsBw.RunWorkerCompleted += udpate_BeginPdfOnlyBackups_menuItem;
}
private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __)
{
runBackupCountsAgain = true;
if (!updateCountsBw.IsBusy)
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
e.Result = LibraryCommands.GetCounts();
}
}
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults;
}
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
private void updateBottomBookNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var formatString
= !libraryStats.HasBookResults ? "No books. Begin by importing your library"
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
: libraryStats.HasPendingBooks ? backupsCountsLbl_Format
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
var statusStripText = string.Format(formatString,
libraryStats.booksNoProgress,
libraryStats.booksDownloadedOnly,
libraryStats.booksFullyBackedUp,
libraryStats.booksError);
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
}
// update 'begin book backups' menu item
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.HasPendingBooks
? $"{libraryStats.PendingBooks} remaining"
: "All books have been liberated";
menuStrip1.UIThreadAsync(() =>
{
beginBookBackupsToolStripMenuItem.Format(menuItemText);
beginBookBackupsToolStripMenuItem.Enabled = libraryStats.HasPendingBooks;
});
}
private void updateBottomPdfNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
// don't need to assign the output of Format(). It just makes this logic cleaner
var statusStripText
= !libraryStats.HasPdfResults ? ""
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
}
// update 'begin pdf only backups' menu item
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
menuStrip1.UIThreadAsync(() =>
{
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
});
}
}
}

View File

@@ -28,102 +28,100 @@
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.gridPanel = new System.Windows.Forms.Panel();
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.autoScanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateVisible2ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.scanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.visibleBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateVisibleToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setDownloadedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.addFilterBtn = new System.Windows.Forms.Button();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
this.SuspendLayout();
//
// gridPanel
//
this.gridPanel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));
this.filterHelpBtn = new System.Windows.Forms.Button();
this.filterBtn = new System.Windows.Forms.Button();
this.filterSearchTb = new System.Windows.Forms.TextBox();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.importToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.autoScanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.noAccountsYetAddAccountToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.scanLibraryOfSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeLibraryBooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeAllAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeSomeAccountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.beginBookBackupsToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.beginPdfBackupsToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.convertAllM4bToMp3ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.liberateVisibleToolStripMenuItem_LiberateMenu = new LibationWinForms.FormattableToolStripMenuItem();
this.exportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.exportLibraryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.quickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.firstFilterIsDefaultToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.editQuickFiltersToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.scanningToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.visibleBooksToolStripMenuItem = new LibationWinForms.FormattableToolStripMenuItem();
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu = new LibationWinForms.FormattableToolStripMenuItem();
this.replaceTagsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.setDownloadedToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.basicSettingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
this.aboutToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
this.statusStrip1 = new System.Windows.Forms.StatusStrip();
this.visibleCountLbl = new LibationWinForms.FormattableToolStripStatusLabel();
this.springLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.backupsCountsLbl = new System.Windows.Forms.ToolStripStatusLabel();
this.pdfsCountsLbl = new LibationWinForms.FormattableToolStripStatusLabel();
this.addQuickFilterBtn = new System.Windows.Forms.Button();
this.splitContainer1 = new System.Windows.Forms.SplitContainer();
this.panel1 = new System.Windows.Forms.Panel();
this.productsDisplay = new LibationWinForms.GridView.ProductsDisplay();
this.toggleQueueHideBtn = new System.Windows.Forms.Button();
this.processBookQueue1 = new LibationWinForms.ProcessQueue.ProcessQueueControl();
this.menuStrip1.SuspendLayout();
this.statusStrip1.SuspendLayout();
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit();
this.splitContainer1.Panel1.SuspendLayout();
this.splitContainer1.Panel2.SuspendLayout();
this.splitContainer1.SuspendLayout();
this.panel1.SuspendLayout();
this.SuspendLayout();
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(15, 3);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(15, 3, 4, 3);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(26, 27);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(916, 3);
this.filterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(88, 27);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.gridPanel.Location = new System.Drawing.Point(34, 178);
this.gridPanel.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.gridPanel.Name = "gridPanel";
this.gridPanel.Size = new System.Drawing.Size(2378, 1216);
this.gridPanel.TabIndex = 5;
//
// filterHelpBtn
//
this.filterHelpBtn.Location = new System.Drawing.Point(34, 85);
this.filterHelpBtn.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.filterHelpBtn.Name = "filterHelpBtn";
this.filterHelpBtn.Size = new System.Drawing.Size(63, 74);
this.filterHelpBtn.TabIndex = 3;
this.filterHelpBtn.Text = "?";
this.filterHelpBtn.UseVisualStyleBackColor = true;
this.filterHelpBtn.Click += new System.EventHandler(this.filterHelpBtn_Click);
//
// filterBtn
//
this.filterBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.filterBtn.Location = new System.Drawing.Point(2198, 85);
this.filterBtn.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.filterBtn.Name = "filterBtn";
this.filterBtn.Size = new System.Drawing.Size(214, 74);
this.filterBtn.TabIndex = 2;
this.filterBtn.Text = "Filter";
this.filterBtn.UseVisualStyleBackColor = true;
this.filterBtn.Click += new System.EventHandler(this.filterBtn_Click);
//
// filterSearchTb
//
this.filterSearchTb.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.filterSearchTb.Location = new System.Drawing.Point(527, 90);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(1648, 47);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.filterSearchTb.Location = new System.Drawing.Point(196, 7);
this.filterSearchTb.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.filterSearchTb.Name = "filterSearchTb";
this.filterSearchTb.Size = new System.Drawing.Size(712, 23);
this.filterSearchTb.TabIndex = 1;
this.filterSearchTb.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.filterSearchTb_KeyPress);
//
// menuStrip1
//
this.menuStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem,
this.liberateToolStripMenuItem,
this.exportToolStripMenuItem,
@@ -131,344 +129,425 @@
this.scanningToolStripMenuItem,
this.visibleBooksToolStripMenuItem,
this.settingsToolStripMenuItem});
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(17, 5, 0, 5);
this.menuStrip1.Size = new System.Drawing.Size(2446, 58);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.menuStrip1.Location = new System.Drawing.Point(0, 0);
this.menuStrip1.Name = "menuStrip1";
this.menuStrip1.Padding = new System.Windows.Forms.Padding(7, 2, 0, 2);
this.menuStrip1.Size = new System.Drawing.Size(1061, 24);
this.menuStrip1.TabIndex = 0;
this.menuStrip1.Text = "menuStrip1";
//
// importToolStripMenuItem
//
this.importToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.autoScanLibraryToolStripMenuItem,
this.noAccountsYetAddAccountToolStripMenuItem,
this.scanLibraryToolStripMenuItem,
this.scanLibraryOfAllAccountsToolStripMenuItem,
this.scanLibraryOfSomeAccountsToolStripMenuItem,
this.removeLibraryBooksToolStripMenuItem});
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(132, 48);
this.importToolStripMenuItem.Text = "&Import";
//
// autoScanLibraryToolStripMenuItem
//
this.autoScanLibraryToolStripMenuItem.Name = "autoScanLibraryToolStripMenuItem";
this.autoScanLibraryToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.autoScanLibraryToolStripMenuItem.Text = "A&uto Scan Library";
this.autoScanLibraryToolStripMenuItem.Click += new System.EventHandler(this.autoScanLibraryToolStripMenuItem_Click);
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.importToolStripMenuItem.Name = "importToolStripMenuItem";
this.importToolStripMenuItem.Size = new System.Drawing.Size(55, 20);
this.importToolStripMenuItem.Text = "&Import";
//
// autoScanLibraryToolStripMenuItem
//
this.autoScanLibraryToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.autoScanLibraryToolStripMenuItem.Name = "autoScanLibraryToolStripMenuItem";
this.autoScanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.autoScanLibraryToolStripMenuItem.Text = "A&uto Scan Library";
this.autoScanLibraryToolStripMenuItem.Click += new System.EventHandler(this.autoScanLibraryToolStripMenuItem_Click);
//
// noAccountsYetAddAccountToolStripMenuItem
//
this.noAccountsYetAddAccountToolStripMenuItem.Name = "noAccountsYetAddAccountToolStripMenuItem";
this.noAccountsYetAddAccountToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.noAccountsYetAddAccountToolStripMenuItem.Text = "No accounts yet. A&dd Account...";
this.noAccountsYetAddAccountToolStripMenuItem.Click += new System.EventHandler(this.noAccountsYetAddAccountToolStripMenuItem_Click);
//
// scanLibraryToolStripMenuItem
//
this.scanLibraryToolStripMenuItem.Name = "scanLibraryToolStripMenuItem";
this.scanLibraryToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryToolStripMenuItem.Text = "Scan &Library";
this.scanLibraryToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryToolStripMenuItem_Click);
//
// scanLibraryOfAllAccountsToolStripMenuItem
//
this.scanLibraryOfAllAccountsToolStripMenuItem.Name = "scanLibraryOfAllAccountsToolStripMenuItem";
this.scanLibraryOfAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfAllAccountsToolStripMenuItem.Text = "Scan Library of &All Accounts";
this.scanLibraryOfAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfAllAccountsToolStripMenuItem_Click);
//
// scanLibraryOfSomeAccountsToolStripMenuItem
//
this.scanLibraryOfSomeAccountsToolStripMenuItem.Name = "scanLibraryOfSomeAccountsToolStripMenuItem";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.scanLibraryOfSomeAccountsToolStripMenuItem.Text = "Scan Library of &Some Accounts...";
this.scanLibraryOfSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.scanLibraryOfSomeAccountsToolStripMenuItem_Click);
//
// removeLibraryBooksToolStripMenuItem
//
this.removeLibraryBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeAllAccountsToolStripMenuItem,
this.removeSomeAccountsToolStripMenuItem});
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(613, 54);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(390, 54);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(390, 54);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.removeLibraryBooksToolStripMenuItem.Name = "removeLibraryBooksToolStripMenuItem";
this.removeLibraryBooksToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
this.removeLibraryBooksToolStripMenuItem.Text = "Remove Library Books";
this.removeLibraryBooksToolStripMenuItem.Click += new System.EventHandler(this.removeLibraryBooksToolStripMenuItem_Click);
//
// removeAllAccountsToolStripMenuItem
//
this.removeAllAccountsToolStripMenuItem.Name = "removeAllAccountsToolStripMenuItem";
this.removeAllAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeAllAccountsToolStripMenuItem.Text = "All Accounts";
this.removeAllAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeAllAccountsToolStripMenuItem_Click);
//
// removeSomeAccountsToolStripMenuItem
//
this.removeSomeAccountsToolStripMenuItem.Name = "removeSomeAccountsToolStripMenuItem";
this.removeSomeAccountsToolStripMenuItem.Size = new System.Drawing.Size(157, 22);
this.removeSomeAccountsToolStripMenuItem.Text = "Some Accounts";
this.removeSomeAccountsToolStripMenuItem.Click += new System.EventHandler(this.removeSomeAccountsToolStripMenuItem_Click);
//
// liberateToolStripMenuItem
//
this.liberateToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.beginBookBackupsToolStripMenuItem,
this.beginPdfBackupsToolStripMenuItem,
this.convertAllM4bToMp3ToolStripMenuItem,
this.liberateVisible2ToolStripMenuItem});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(148, 48);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(728, 54);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(728, 54);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(728, 54);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all &M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// liberateVisible2ToolStripMenuItem
//
this.liberateVisible2ToolStripMenuItem.Name = "liberateVisible2ToolStripMenuItem";
this.liberateVisible2ToolStripMenuItem.Size = new System.Drawing.Size(728, 54);
this.liberateVisible2ToolStripMenuItem.Text = "Liberate &Visible Books: {0}";
this.liberateVisible2ToolStripMenuItem.Click += new System.EventHandler(this.liberateVisible);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateVisibleToolStripMenuItem_LiberateMenu});
this.liberateToolStripMenuItem.Name = "liberateToolStripMenuItem";
this.liberateToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.liberateToolStripMenuItem.Text = "&Liberate";
//
// beginBookBackupsToolStripMenuItem
//
this.beginBookBackupsToolStripMenuItem.FormatText = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Name = "beginBookBackupsToolStripMenuItem";
this.beginBookBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginBookBackupsToolStripMenuItem.Text = "Begin &Book and PDF Backups: {0}";
this.beginBookBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginBookBackupsToolStripMenuItem_Click);
//
// beginPdfBackupsToolStripMenuItem
//
this.beginPdfBackupsToolStripMenuItem.FormatText = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Name = "beginPdfBackupsToolStripMenuItem";
this.beginPdfBackupsToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.beginPdfBackupsToolStripMenuItem.Text = "Begin &PDF Only Backups: {0}";
this.beginPdfBackupsToolStripMenuItem.Click += new System.EventHandler(this.beginPdfBackupsToolStripMenuItem_Click);
//
// convertAllM4bToMp3ToolStripMenuItem
//
this.convertAllM4bToMp3ToolStripMenuItem.Name = "convertAllM4bToMp3ToolStripMenuItem";
this.convertAllM4bToMp3ToolStripMenuItem.Size = new System.Drawing.Size(293, 22);
this.convertAllM4bToMp3ToolStripMenuItem.Text = "Convert all &M4b to Mp3 [Long-running]...";
this.convertAllM4bToMp3ToolStripMenuItem.Click += new System.EventHandler(this.convertAllM4bToMp3ToolStripMenuItem_Click);
//
// liberateVisibleToolStripMenuItem_LiberateMenu
//
this.liberateVisibleToolStripMenuItem_LiberateMenu.FormatText = "Liberate &Visible Books: {0}";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Name = "liberateVisibleToolStripMenuItem_LiberateMenu";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Size = new System.Drawing.Size(293, 22);
this.liberateVisibleToolStripMenuItem_LiberateMenu.Text = "Liberate &Visible Books: {0}";
this.liberateVisibleToolStripMenuItem_LiberateMenu.Click += new System.EventHandler(this.liberateVisible);
//
// exportToolStripMenuItem
//
this.exportToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportLibraryToolStripMenuItem});
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(127, 48);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(387, 54);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.exportToolStripMenuItem.Name = "exportToolStripMenuItem";
this.exportToolStripMenuItem.Size = new System.Drawing.Size(53, 20);
this.exportToolStripMenuItem.Text = "E&xport";
//
// exportLibraryToolStripMenuItem
//
this.exportLibraryToolStripMenuItem.Name = "exportLibraryToolStripMenuItem";
this.exportLibraryToolStripMenuItem.Size = new System.Drawing.Size(156, 22);
this.exportLibraryToolStripMenuItem.Text = "E&xport Library...";
this.exportLibraryToolStripMenuItem.Click += new System.EventHandler(this.exportLibraryToolStripMenuItem_Click);
//
// quickFiltersToolStripMenuItem
//
this.quickFiltersToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.firstFilterIsDefaultToolStripMenuItem,
this.editQuickFiltersToolStripMenuItem,
this.toolStripSeparator1});
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(204, 48);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(639, 54);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.FirstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(639, 54);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.EditQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(636, 6);
//
// scanningToolStripMenuItem
//
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(224, 48);
this.scanningToolStripMenuItem.Text = "Scanning...";
this.scanningToolStripMenuItem.Visible = false;
//
// visibleBooksToolStripMenuItem
//
this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateVisibleToolStripMenuItem,
this.quickFiltersToolStripMenuItem.Name = "quickFiltersToolStripMenuItem";
this.quickFiltersToolStripMenuItem.Size = new System.Drawing.Size(84, 20);
this.quickFiltersToolStripMenuItem.Text = "Quick &Filters";
//
// firstFilterIsDefaultToolStripMenuItem
//
this.firstFilterIsDefaultToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.firstFilterIsDefaultToolStripMenuItem.Name = "firstFilterIsDefaultToolStripMenuItem";
this.firstFilterIsDefaultToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.firstFilterIsDefaultToolStripMenuItem.Text = "Start Libation with 1st filter &Default";
this.firstFilterIsDefaultToolStripMenuItem.Click += new System.EventHandler(this.firstFilterIsDefaultToolStripMenuItem_Click);
//
// editQuickFiltersToolStripMenuItem
//
this.editQuickFiltersToolStripMenuItem.Name = "editQuickFiltersToolStripMenuItem";
this.editQuickFiltersToolStripMenuItem.Size = new System.Drawing.Size(256, 22);
this.editQuickFiltersToolStripMenuItem.Text = "&Edit quick filters...";
this.editQuickFiltersToolStripMenuItem.Click += new System.EventHandler(this.editQuickFiltersToolStripMenuItem_Click);
//
// toolStripSeparator1
//
this.toolStripSeparator1.Name = "toolStripSeparator1";
this.toolStripSeparator1.Size = new System.Drawing.Size(253, 6);
//
// scanningToolStripMenuItem
//
this.scanningToolStripMenuItem.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
this.scanningToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Image = global::LibationWinForms.Properties.Resources.import_16x16;
this.scanningToolStripMenuItem.ImageScaling = System.Windows.Forms.ToolStripItemImageScaling.None;
this.scanningToolStripMenuItem.Name = "scanningToolStripMenuItem";
this.scanningToolStripMenuItem.Size = new System.Drawing.Size(93, 20);
this.scanningToolStripMenuItem.Text = "Scanning...";
this.scanningToolStripMenuItem.Visible = false;
//
// visibleBooksToolStripMenuItem
//
this.visibleBooksToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu,
this.replaceTagsToolStripMenuItem,
this.setDownloadedToolStripMenuItem,
this.removeToolStripMenuItem});
this.visibleBooksToolStripMenuItem.Name = "visibleBooksToolStripMenuItem";
this.visibleBooksToolStripMenuItem.Size = new System.Drawing.Size(267, 48);
this.visibleBooksToolStripMenuItem.Text = "&Visible Books: {0}";
//
// liberateVisibleToolStripMenuItem
//
this.liberateVisibleToolStripMenuItem.Name = "liberateVisibleToolStripMenuItem";
this.liberateVisibleToolStripMenuItem.Size = new System.Drawing.Size(525, 54);
this.liberateVisibleToolStripMenuItem.Text = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem.Click += new System.EventHandler(this.liberateVisible);
//
// replaceTagsToolStripMenuItem
//
this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem";
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(525, 54);
this.replaceTagsToolStripMenuItem.Text = "Replace &Tags...";
this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click);
//
// setDownloadedToolStripMenuItem
//
this.setDownloadedToolStripMenuItem.Name = "setDownloadedToolStripMenuItem";
this.setDownloadedToolStripMenuItem.Size = new System.Drawing.Size(525, 54);
this.setDownloadedToolStripMenuItem.Text = "Set \'&Downloaded\' status...";
this.setDownloadedToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedToolStripMenuItem_Click);
//
// removeToolStripMenuItem
//
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
this.removeToolStripMenuItem.Size = new System.Drawing.Size(525, 54);
this.removeToolStripMenuItem.Text = "&Remove from library...";
this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.visibleBooksToolStripMenuItem.FormatText = "&Visible Books: {0}";
this.visibleBooksToolStripMenuItem.Name = "visibleBooksToolStripMenuItem";
this.visibleBooksToolStripMenuItem.Size = new System.Drawing.Size(108, 20);
this.visibleBooksToolStripMenuItem.Text = "&Visible Books: {0}";
//
// liberateVisibleToolStripMenuItem_VisibleBooksMenu
//
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.FormatText = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Name = "liberateVisibleToolStripMenuItem_VisibleBooksMenu";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Size = new System.Drawing.Size(209, 22);
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "&Liberate: {0}";
this.liberateVisibleToolStripMenuItem_VisibleBooksMenu.Click += new System.EventHandler(this.liberateVisible);
//
// replaceTagsToolStripMenuItem
//
this.replaceTagsToolStripMenuItem.Name = "replaceTagsToolStripMenuItem";
this.replaceTagsToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.replaceTagsToolStripMenuItem.Text = "Replace &Tags...";
this.replaceTagsToolStripMenuItem.Click += new System.EventHandler(this.replaceTagsToolStripMenuItem_Click);
//
// setDownloadedToolStripMenuItem
//
this.setDownloadedToolStripMenuItem.Name = "setDownloadedToolStripMenuItem";
this.setDownloadedToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.setDownloadedToolStripMenuItem.Text = "Set \'&Downloaded\' status...";
this.setDownloadedToolStripMenuItem.Click += new System.EventHandler(this.setDownloadedToolStripMenuItem_Click);
//
// removeToolStripMenuItem
//
this.removeToolStripMenuItem.Name = "removeToolStripMenuItem";
this.removeToolStripMenuItem.Size = new System.Drawing.Size(209, 22);
this.removeToolStripMenuItem.Text = "&Remove from library...";
this.removeToolStripMenuItem.Click += new System.EventHandler(this.removeToolStripMenuItem_Click);
//
// settingsToolStripMenuItem
//
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.accountsToolStripMenuItem,
this.basicSettingsToolStripMenuItem,
this.toolStripSeparator2,
this.aboutToolStripMenuItem});
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(149, 48);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(327, 54);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(327, 54);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(324, 6);
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(327, 54);
this.aboutToolStripMenuItem.Text = "A&bout...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
this.settingsToolStripMenuItem.Size = new System.Drawing.Size(61, 20);
this.settingsToolStripMenuItem.Text = "&Settings";
//
// accountsToolStripMenuItem
//
this.accountsToolStripMenuItem.Name = "accountsToolStripMenuItem";
this.accountsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.accountsToolStripMenuItem.Text = "&Accounts...";
this.accountsToolStripMenuItem.Click += new System.EventHandler(this.accountsToolStripMenuItem_Click);
//
// basicSettingsToolStripMenuItem
//
this.basicSettingsToolStripMenuItem.Name = "basicSettingsToolStripMenuItem";
this.basicSettingsToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.basicSettingsToolStripMenuItem.Text = "&Settings...";
this.basicSettingsToolStripMenuItem.Click += new System.EventHandler(this.basicSettingsToolStripMenuItem_Click);
//
// toolStripSeparator2
//
this.toolStripSeparator2.Name = "toolStripSeparator2";
this.toolStripSeparator2.Size = new System.Drawing.Size(130, 6);
//
// aboutToolStripMenuItem
//
this.aboutToolStripMenuItem.Name = "aboutToolStripMenuItem";
this.aboutToolStripMenuItem.Size = new System.Drawing.Size(133, 22);
this.aboutToolStripMenuItem.Text = "A&bout...";
this.aboutToolStripMenuItem.Click += new System.EventHandler(this.aboutToolStripMenuItem_Click);
//
// statusStrip1
//
this.statusStrip1.ImageScalingSize = new System.Drawing.Size(40, 40);
this.statusStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] {
this.visibleCountLbl,
this.springLbl,
this.backupsCountsLbl,
this.pdfsCountsLbl});
this.statusStrip1.Location = new System.Drawing.Point(0, 1419);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(2, 0, 39, 0);
this.statusStrip1.Size = new System.Drawing.Size(2446, 54);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(136, 41);
this.visibleCountLbl.Text = "Visible: 0";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(1299, 41);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(544, 41);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(426, 41);
this.pdfsCountsLbl.Text = "| [Calculating backed up PDFs]";
//
// addFilterBtn
//
this.addFilterBtn.Location = new System.Drawing.Point(114, 85);
this.addFilterBtn.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.addFilterBtn.Name = "addFilterBtn";
this.addFilterBtn.Size = new System.Drawing.Size(396, 74);
this.addFilterBtn.TabIndex = 4;
this.addFilterBtn.Text = "Add To Quick Filters";
this.addFilterBtn.UseVisualStyleBackColor = true;
this.addFilterBtn.Click += new System.EventHandler(this.AddFilterBtn_Click);
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(17F, 41F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(2446, 1473);
this.Controls.Add(this.filterBtn);
this.Controls.Add(this.addFilterBtn);
this.Controls.Add(this.filterSearchTb);
this.Controls.Add(this.filterHelpBtn);
this.Controls.Add(this.statusStrip1);
this.Controls.Add(this.gridPanel);
this.Controls.Add(this.menuStrip1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(10, 8, 10, 8);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.ResumeLayout(false);
this.PerformLayout();
this.statusStrip1.Location = new System.Drawing.Point(0, 618);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Padding = new System.Windows.Forms.Padding(1, 0, 16, 0);
this.statusStrip1.Size = new System.Drawing.Size(1061, 22);
this.statusStrip1.TabIndex = 6;
this.statusStrip1.Text = "statusStrip1";
//
// visibleCountLbl
//
this.visibleCountLbl.FormatText = "Visible: {0}";
this.visibleCountLbl.Name = "visibleCountLbl";
this.visibleCountLbl.Size = new System.Drawing.Size(61, 17);
this.visibleCountLbl.Text = "Visible: {0}";
//
// springLbl
//
this.springLbl.Name = "springLbl";
this.springLbl.Size = new System.Drawing.Size(547, 17);
this.springLbl.Spring = true;
//
// backupsCountsLbl
//
this.backupsCountsLbl.Name = "backupsCountsLbl";
this.backupsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.backupsCountsLbl.Text = "[Calculating backed up book quantities]";
//
// pdfsCountsLbl
//
this.pdfsCountsLbl.FormatText = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
this.pdfsCountsLbl.Name = "pdfsCountsLbl";
this.pdfsCountsLbl.Size = new System.Drawing.Size(218, 17);
this.pdfsCountsLbl.Text = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
//
// addQuickFilterBtn
//
this.addQuickFilterBtn.Location = new System.Drawing.Point(50, 3);
this.addQuickFilterBtn.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.addQuickFilterBtn.Name = "addQuickFilterBtn";
this.addQuickFilterBtn.Size = new System.Drawing.Size(137, 27);
this.addQuickFilterBtn.TabIndex = 4;
this.addQuickFilterBtn.Text = "Add To Quick Filters";
this.addQuickFilterBtn.UseVisualStyleBackColor = true;
this.addQuickFilterBtn.Click += new System.EventHandler(this.addQuickFilterBtn_Click);
//
// splitContainer1
//
this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill;
this.splitContainer1.Location = new System.Drawing.Point(0, 0);
this.splitContainer1.Name = "splitContainer1";
//
// splitContainer1.Panel1
//
this.splitContainer1.Panel1.Controls.Add(this.panel1);
this.splitContainer1.Panel1.Controls.Add(this.menuStrip1);
this.splitContainer1.Panel1.Controls.Add(this.statusStrip1);
//
// splitContainer1.Panel2
//
this.splitContainer1.Panel2.Controls.Add(this.processBookQueue1);
this.splitContainer1.Size = new System.Drawing.Size(1463, 640);
this.splitContainer1.SplitterDistance = 1061;
this.splitContainer1.SplitterWidth = 8;
this.splitContainer1.TabIndex = 7;
//
// panel1
//
this.panel1.AutoSizeMode = System.Windows.Forms.AutoSizeMode.GrowAndShrink;
this.panel1.Controls.Add(this.productsDisplay);
this.panel1.Controls.Add(this.toggleQueueHideBtn);
this.panel1.Controls.Add(this.addQuickFilterBtn);
this.panel1.Controls.Add(this.filterHelpBtn);
this.panel1.Controls.Add(this.filterSearchTb);
this.panel1.Controls.Add(this.filterBtn);
this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
this.panel1.Location = new System.Drawing.Point(0, 24);
this.panel1.Margin = new System.Windows.Forms.Padding(0);
this.panel1.Name = "panel1";
this.panel1.Size = new System.Drawing.Size(1061, 594);
this.panel1.TabIndex = 7;
//
// productsDisplay
//
this.productsDisplay.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.productsDisplay.AutoScroll = true;
this.productsDisplay.Location = new System.Drawing.Point(15, 36);
this.productsDisplay.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.productsDisplay.Name = "productsDisplay";
this.productsDisplay.Size = new System.Drawing.Size(1031, 555);
this.productsDisplay.TabIndex = 9;
this.productsDisplay.VisibleCountChanged += new System.EventHandler<int>(this.productsDisplay_VisibleCountChanged);
this.productsDisplay.LiberateClicked += new System.EventHandler<DataLayer.LibraryBook>(this.ProductsDisplay_LiberateClicked);
this.productsDisplay.InitialLoaded += new System.EventHandler(this.productsDisplay_InitialLoaded);
//
// toggleQueueHideBtn
//
this.toggleQueueHideBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.toggleQueueHideBtn.Location = new System.Drawing.Point(1013, 3);
this.toggleQueueHideBtn.Margin = new System.Windows.Forms.Padding(4, 3, 15, 3);
this.toggleQueueHideBtn.Name = "toggleQueueHideBtn";
this.toggleQueueHideBtn.Size = new System.Drawing.Size(33, 27);
this.toggleQueueHideBtn.TabIndex = 8;
this.toggleQueueHideBtn.Text = "❱❱❱";
this.toggleQueueHideBtn.UseVisualStyleBackColor = true;
this.toggleQueueHideBtn.Click += new System.EventHandler(this.ToggleQueueHideBtn_Click);
//
// processBookQueue1
//
this.processBookQueue1.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.processBookQueue1.Dock = System.Windows.Forms.DockStyle.Fill;
this.processBookQueue1.Location = new System.Drawing.Point(0, 0);
this.processBookQueue1.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
this.processBookQueue1.Name = "processBookQueue1";
this.processBookQueue1.Size = new System.Drawing.Size(394, 640);
this.processBookQueue1.TabIndex = 0;
//
// Form1
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(1463, 640);
this.Controls.Add(this.splitContainer1);
this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon")));
this.MainMenuStrip = this.menuStrip1;
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "Form1";
this.Text = "Libation: Liberate your Library";
this.Load += new System.EventHandler(this.Form1_Load);
this.menuStrip1.ResumeLayout(false);
this.menuStrip1.PerformLayout();
this.statusStrip1.ResumeLayout(false);
this.statusStrip1.PerformLayout();
this.splitContainer1.Panel1.ResumeLayout(false);
this.splitContainer1.Panel1.PerformLayout();
this.splitContainer1.Panel2.ResumeLayout(false);
((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit();
this.splitContainer1.ResumeLayout(false);
this.panel1.ResumeLayout(false);
this.panel1.PerformLayout();
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Panel gridPanel;
private System.Windows.Forms.MenuStrip menuStrip1;
private System.Windows.Forms.ToolStripMenuItem importToolStripMenuItem;
private System.Windows.Forms.StatusStrip statusStrip1;
private System.Windows.Forms.ToolStripStatusLabel springLbl;
private System.Windows.Forms.ToolStripStatusLabel visibleCountLbl;
private LibationWinForms.FormattableToolStripStatusLabel visibleCountLbl;
private System.Windows.Forms.ToolStripMenuItem liberateToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel backupsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginBookBackupsToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel pdfsCountsLbl;
private System.Windows.Forms.ToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem beginBookBackupsToolStripMenuItem;
private LibationWinForms.FormattableToolStripStatusLabel pdfsCountsLbl;
private LibationWinForms.FormattableToolStripMenuItem beginPdfBackupsToolStripMenuItem;
private System.Windows.Forms.TextBox filterSearchTb;
private System.Windows.Forms.Button filterBtn;
private System.Windows.Forms.Button filterHelpBtn;
@@ -476,7 +555,7 @@
private System.Windows.Forms.ToolStripMenuItem scanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem quickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem firstFilterIsDefaultToolStripMenuItem;
private System.Windows.Forms.Button addFilterBtn;
private System.Windows.Forms.Button addQuickFilterBtn;
private System.Windows.Forms.ToolStripMenuItem editQuickFiltersToolStripMenuItem;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem basicSettingsToolStripMenuItem;
@@ -494,11 +573,16 @@
private System.Windows.Forms.ToolStripMenuItem aboutToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem scanningToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem autoScanLibraryToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem visibleBooksToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem liberateVisibleToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem visibleBooksToolStripMenuItem;
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_VisibleBooksMenu;
private System.Windows.Forms.ToolStripMenuItem replaceTagsToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem setDownloadedToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
private System.Windows.Forms.ToolStripMenuItem liberateVisible2ToolStripMenuItem;
}
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
private System.Windows.Forms.SplitContainer splitContainer1;
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
private System.Windows.Forms.Panel panel1;
private System.Windows.Forms.Button toggleQueueHideBtn;
private LibationWinForms.GridView.ProductsDisplay productsDisplay;
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Windows.Forms;
using ApplicationServices;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_Export() { }
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
var saveFileDialog = new SaveFileDialog
{
Title = "Where to export Library",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
LibraryExporter.ToXlsx(saveFileDialog.FileName);
break;
case 2: // csv
LibraryExporter.ToCsv(saveFileDialog.FileName);
break;
case 3: // json
LibraryExporter.ToJson(saveFileDialog.FileName);
break;
}
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert("Error attempting to export your library.", "Error exporting", ex);
}
}
}
}

View File

@@ -0,0 +1,45 @@
using System;
using System.Windows.Forms;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1
{
protected void Configure_Filter() { }
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Return)
{
performFilter(this.filterSearchTb.Text);
// silence the 'ding'
e.Handled = true;
}
}
private void filterBtn_Click(object sender, EventArgs e) => performFilter(this.filterSearchTb.Text);
private string lastGoodFilter = "";
private void performFilter(string filterString)
{
this.filterSearchTb.Text = filterString;
try
{
productsDisplay.Filter(filterString);
lastGoodFilter = filterString;
}
catch (Exception ex)
{
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
// re-apply last good filter
performFilter(lastGoodFilter);
}
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_Liberate() { }
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated || lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.NotLiberated)));
}
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadPdf(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.PdfStatus is DataLayer.LiberatedStatus.NotLiberated)));
}
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
{
var result = MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddConvertMp3(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
.Where(lb => lb.Book.UserDefinedItem.BookStatus is DataLayer.LiberatedStatus.Liberated)));
}
//Only Queue Liberated books for conversion. This isn't a perfect filter, but it's better than nothing.
}
}
}

View File

@@ -0,0 +1,107 @@
using DataLayer;
using Dinah.Core;
using LibationFileManager;
using LibationWinForms.ProcessQueue;
using System;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms
{
public partial class Form1
{
int WidthChange = 0;
private void Configure_ProcessQueue()
{
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
SetQueueCollapseState(coppalseState);
}
private void ProductsDisplay_LiberateClicked(object sender, LibraryBook e)
{
if (e.Book.UserDefinedItem.BookStatus != LiberatedStatus.Liberated)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadDecrypt(e);
}
else if (e.Book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.NotLiberated)
{
SetQueueCollapseState(false);
processBookQueue1.AddDownloadPdf(e);
}
else if (e.Book.Audio_Exists())
{
// liberated: open explorer to file
var filePath = AudibleFileStorage.Audio.GetPath(e.Book.AudibleProductId);
if (!Go.To.File(filePath))
{
var suffix = string.IsNullOrWhiteSpace(filePath) ? "" : $":\r\n{filePath}";
MessageBox.Show($"File not found" + suffix);
}
}
}
private void SetQueueCollapseState(bool collapsed)
{
if (collapsed && !splitContainer1.Panel2Collapsed)
{
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
splitContainer1.Panel2Collapsed = true;
Width -= WidthChange;
}
else if (!collapsed && splitContainer1.Panel2Collapsed)
{
Width += WidthChange;
splitContainer1.Panel2.Controls.Add(processBookQueue1);
splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true;
}
toggleQueueHideBtn.Text = splitContainer1.Panel2Collapsed ? "❰❰❰" : "❱❱❱";
}
private void ToggleQueueHideBtn_Click(object sender, EventArgs e)
{
SetQueueCollapseState(!splitContainer1.Panel2Collapsed);
Configuration.Instance.SetObject(nameof(splitContainer1.Panel2Collapsed), splitContainer1.Panel2Collapsed);
}
private void ProcessBookQueue1_PopOut(object sender, EventArgs e)
{
ProcessBookForm dockForm = new();
dockForm.WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
dockForm.RestoreSizeAndLocation(Configuration.Instance);
dockForm.FormClosing += DockForm_FormClosing;
splitContainer1.Panel2.Controls.Remove(processBookQueue1);
splitContainer1.Panel2Collapsed = true;
processBookQueue1.popoutBtn.Visible = false;
dockForm.PassControl(processBookQueue1);
dockForm.Show();
this.Width -= dockForm.WidthChange;
toggleQueueHideBtn.Visible = false;
int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
filterBtn.Location= new System.Drawing.Point(filterBtn.Location.X + deltax, filterBtn.Location.Y);
filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X + deltax, filterSearchTb.Location.Y);
}
private void DockForm_FormClosing(object sender, FormClosingEventArgs e)
{
if (sender is ProcessBookForm dockForm)
{
this.Width += dockForm.WidthChange;
splitContainer1.Panel2.Controls.Add(dockForm.RegainControl());
splitContainer1.Panel2Collapsed = false;
processBookQueue1.popoutBtn.Visible = true;
dockForm.SaveSizeAndLocation(Configuration.Instance);
this.Focus();
toggleQueueHideBtn.Visible = true;
int deltax = filterBtn.Margin.Right + toggleQueueHideBtn.Width + toggleQueueHideBtn.Margin.Left;
filterBtn.Location = new System.Drawing.Point(filterBtn.Location.X - deltax, filterBtn.Location.Y);
filterSearchTb.Location = new System.Drawing.Point(filterSearchTb.Location.X - deltax, filterSearchTb.Location.Y);
}
}
}
}

View File

@@ -0,0 +1,60 @@
using System;
using System.Linq;
using System.Windows.Forms;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_QuickFilters()
{
Load += updateFirstFilterIsDefaultToolStripMenuItem;
Load += updateFiltersMenu;
QuickFilters.UseDefaultChanged += updateFirstFilterIsDefaultToolStripMenuItem;
QuickFilters.Updated += updateFiltersMenu;
}
private object quickFilterTag { get; } = new();
private void updateFiltersMenu(object _ = null, object __ = null)
{
// remove old
var removeUs = quickFiltersToolStripMenuItem.DropDownItems
.Cast<ToolStripItem>()
.Where(item => item.Tag == quickFilterTag)
.ToList();
foreach (var item in removeUs)
quickFiltersToolStripMenuItem.DropDownItems.Remove(item);
// re-populate
var index = 0;
foreach (var filter in QuickFilters.Filters)
{
var quickFilterMenuItem = new ToolStripMenuItem
{
Tag = quickFilterTag,
Text = $"&{++index}: {filter}"
};
quickFilterMenuItem.Click += (_, __) => performFilter(filter);
quickFiltersToolStripMenuItem.DropDownItems.Add(quickFilterMenuItem);
}
}
private void updateFirstFilterIsDefaultToolStripMenuItem(object sender, EventArgs e)
=> firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
private void firstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
=> QuickFilters.UseDefault = !firstFilterIsDefaultToolStripMenuItem.Checked;
private void addQuickFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text);
private void editQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters().ShowDialog();
private void productsDisplay_InitialLoaded(object sender, EventArgs e)
{
if (QuickFilters.UseDefault)
performFilter(QuickFilters.Filters.FirstOrDefault());
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using LibationFileManager;
namespace LibationWinForms
{
// This is for the auto-scanner. It is unrelated to manual scanning/import
public partial class Form1
{
private InterruptableTimer autoScanTimer;
private void Configure_ScanAuto()
{
// creating InterruptableTimer inside 'Configure_' is a break from the pattern. As long as no one else needs to access or subscribe to it, this is ok
var hours = 0;
var minutes = 5;
var seconds = 0;
var _5_minutes = new TimeSpan(hours, minutes, seconds);
autoScanTimer = new InterruptableTimer(_5_minutes);
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
autoScanTimer.Elapsed += async (_, __) =>
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.ToArray();
// in autoScan, new books SHALL NOT show dialog
await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts));
};
// load init state to menu checkbox
Load += updateAutoScanLibraryToolStripMenuItem;
// if enabled: begin on load
Load += startAutoScan;
// if new 'default' account is added, run autoscan
AccountsSettingsPersister.Saving += accountsPreSave;
AccountsSettingsPersister.Saved += accountsPostSave;
// when autoscan setting is changed, update menu checkbox and run autoscan
Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem;
Configuration.Instance.AutoScanChanged += startAutoScan;
}
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
return persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.Select(a => (a.AccountId, a.Locale.Name))
.ToList();
}
private void accountsPreSave(object sender = null, EventArgs e = null)
=> preSaveDefaultAccounts = getDefaultAccounts();
private void accountsPostSave(object sender = null, EventArgs e = null)
{
var postSaveDefaultAccounts = getDefaultAccounts();
var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList();
if (newDefaultAccounts.Any())
startAutoScan();
}
private void startAutoScan(object sender = null, EventArgs e = null)
{
if (Configuration.Instance.AutoScan)
autoScanTimer.PerformNow();
else
autoScanTimer.Stop();
}
private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan;
private void autoScanLibraryToolStripMenuItem_Click(object sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked;
}
}

View File

@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
// this is for manual scan/import. Unrelated to auto-scan
public partial class Form1
{
private void Configure_ScanManual()
{
this.Load += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu;
}
private void refreshImportMenu(object _, EventArgs __)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
autoScanLibraryToolStripMenuItem.Visible = count > 0;
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
scanLibraryToolStripMenuItem.Visible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
removeLibraryBooksToolStripMenuItem.Visible = count > 0;
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
removeAllAccountsToolStripMenuItem.Visible = count > 1;
}
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
{
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
new AccountsDialog().ShowDialog();
}
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
}
private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
await scanLibrariesAsync(allAccounts);
}
private async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
}
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog();
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private void scanLibrariesRemovedBooks(params Account[] accounts)
{
using var dialog = new RemoveBooksDialog(accounts);
dialog.ShowDialog();
}
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts)
{
try
{
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using ApplicationServices;
namespace LibationWinForms
{
// This is for the Scanning notificationin the upper right. This shown for manual scanning and auto-scan
public partial class Form1
{
private void Configure_ScanNotification()
{
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
}
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
scanLibraryToolStripMenuItem.Enabled = false;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Visible = true;
this.scanningToolStripMenuItem.Text
= (accountsLength == 1)
? "Scanning..."
: $"Scanning {accountsLength} accounts...";
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{
scanLibraryToolStripMenuItem.Enabled = true;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true;
this.scanningToolStripMenuItem.Visible = false;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Windows.Forms;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_Settings() { }
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog().ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
=> MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
}
}

View File

@@ -0,0 +1,132 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using Dinah.Core.Threading;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1
{
protected void Configure_VisibleBooks()
{
// init formattable
visibleCountLbl.Format(0);
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(0);
liberateVisibleToolStripMenuItem_LiberateMenu.Format(0);
// top menu strip
visibleBooksToolStripMenuItem.Format(0);
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
private async void setLiberatedVisibleMenuItemAsync(object _, object __)
=> await Task.Run(setLiberatedVisibleMenuItem);
void setLiberatedVisibleMenuItem()
{
var notLiberated = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
this.UIThreadSync(() =>
{
if (notLiberated > 0)
{
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Format(notLiberated);
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Enabled = true;
liberateVisibleToolStripMenuItem_LiberateMenu.Format(notLiberated);
liberateVisibleToolStripMenuItem_LiberateMenu.Enabled = true;
}
else
{
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Text = "All visible books are liberated";
liberateVisibleToolStripMenuItem_VisibleBooksMenu.Enabled = false;
liberateVisibleToolStripMenuItem_LiberateMenu.Text = "All visible books are liberated";
liberateVisibleToolStripMenuItem_LiberateMenu.Enabled = false;
}
});
}
private async void liberateVisible(object sender, EventArgs e)
{
SetQueueCollapseState(false);
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(productsDisplay.GetVisible()));
}
private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e)
{
var dialog = new TagsBatchDialog();
var result = dialog.ShowDialog();
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
private void setDownloadedToolStripMenuItem_Click(object sender, EventArgs e)
{
var dialog = new LiberatedStatusBatchDialog();
var result = dialog.ShowDialog();
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
{
var visibleLibraryBooks = productsDisplay.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (confirmationResult != DialogResult.Yes)
return;
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
await LibraryCommands.RemoveBooksAsync(visibleIds);
}
private async void productsDisplay_VisibleCountChanged(object sender, int qty)
{
// bottom-left visible count
visibleCountLbl.Format(qty);
// top menu strip
visibleBooksToolStripMenuItem.Format(qty);
visibleBooksToolStripMenuItem.Enabled = qty > 0;
//Not used for anything?
var notLiberatedCount = productsDisplay.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
await Task.Run(setLiberatedVisibleMenuItem);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ApplicationServices;
using Dinah.Core.Drawing;
using LibationFileManager;
namespace LibationWinForms
{
public partial class Form1
{
private void Configure_NonUI()
{
// init default/placeholder cover art
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
// wire-up event to automatically download after scan.
// winforms only. this should NOT be allowed in cli
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
{
if (!Configuration.Instance.AutoDownloadEpisodes)
return;
var libraryStats = e.Result as LibraryCommands.LibraryStats;
if ((libraryStats.booksNoProgress + libraryStats.pdfsNotDownloaded) > 0)
beginBookBackupsToolStripMenuItem_Click();
};
}
}
}

View File

@@ -4,79 +4,59 @@ using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.Drawing;
using Dinah.Core.Threading;
using LibationFileManager;
using LibationWinForms.Dialogs;
namespace LibationWinForms
{
public partial class Form1 : Form
{
private string beginBookBackupsToolStripMenuItem_format { get; }
private string beginPdfBackupsToolStripMenuItem_format { get; }
private string visibleBooksToolStripMenuItem_format { get; }
private string liberateVisibleToolStripMenuItem_format { get; }
private string liberateVisible2ToolStripMenuItem_format { get; }
private ProductsGrid productsGrid { get; }
public Form1()
{
InitializeComponent();
if (this.DesignMode)
return;
// Pre-requisite:
// Before calling anything else, including subscribing to events, ensure database exists. If we wait and let it happen lazily, race conditions and errors are likely during new installs
using var _ = DbContexts.GetContext();
productsGrid = new ProductsGrid { Dock = DockStyle.Fill };
productsGrid.VisibleCountChanged += (_, qty) => visibleCountLbl.Text = string.Format("Visible: {0}", qty);
gridPanel.Controls.Add(productsGrid);
this.Load += (_, __) =>
{
productsGrid.Display();
// also applies filter. ONLY call AFTER loading grid
loadInitialQuickFilterState();
};
// back up string formats
beginBookBackupsToolStripMenuItem_format = beginBookBackupsToolStripMenuItem.Text;
beginPdfBackupsToolStripMenuItem_format = beginPdfBackupsToolStripMenuItem.Text;
visibleBooksToolStripMenuItem_format = visibleBooksToolStripMenuItem.Text;
liberateVisibleToolStripMenuItem_format = liberateVisibleToolStripMenuItem.Text;
liberateVisible2ToolStripMenuItem_format = liberateVisible2ToolStripMenuItem.Text;
// independent UI updates
this.Load += (_, _) => this.RestoreSizeAndLocation(Configuration.Instance);
this.FormClosing += (_, _) => this.SaveSizeAndLocation(Configuration.Instance);
LibraryCommands.LibrarySizeChanged += (_, __) =>
// this looks like a perfect opportunity to refactor per below.
// since this loses design-time tooling and internal access, for now I'm opting for partial classes
// var modules = new ConfigurableModuleBase[]
// {
// new PictureStorageModule(),
// new BackupCountsModule(),
// new VisibleBooksModule(),
// // ...
// };
// foreach(ConfigurableModuleBase m in modules)
// m.Configure(this);
// these should do nothing interesting yet (storing simple var, subscribe to events) and should never rely on each other for order.
// otherwise, order could be an issue.
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
Configure_BackupCounts();
Configure_ScanAuto();
Configure_ScanNotification();
Configure_VisibleBooks();
Configure_QuickFilters();
Configure_ScanManual();
Configure_Liberate();
Configure_Export();
Configure_Settings();
Configure_ProcessQueue();
Configure_Filter();
// misc which belongs in winforms app but doesn't have a UI element
Configure_NonUI();
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
{
this.UIThreadSync(() => productsGrid.Display());
this.UIThreadAsync(() => doFilter(lastGoodFilter));
};
LibraryCommands.LibrarySizeChanged += setBackupCounts;
this.Load += setBackupCounts;
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
QuickFilters.Updated += updateFiltersMenu;
LibraryCommands.ScanBegin += LibraryCommands_ScanBegin;
LibraryCommands.ScanEnd += LibraryCommands_ScanEnd;
// accounts updated
this.Load += refreshImportMenu;
AccountsSettingsPersister.Saved += refreshImportMenu;
configAndInitAutoScan();
configVisibleBooksMenu();
// init default/placeholder cover art
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
this.Load += (_, __) => productsDisplay.Display();
LibraryCommands.LibrarySizeChanged += (_, __) => this.UIThreadAsync(() => productsDisplay.Display());
}
}
private void Form1_Load(object sender, EventArgs e)
@@ -86,566 +66,5 @@ namespace LibationWinForms
// I'm leaving this empty call here as a reminder that if we use this, it should probably be after DesignMode check
}
#region bottom: backup counts
private System.ComponentModel.BackgroundWorker updateCountsBw;
private bool runBackupCountsAgain;
private void setBackupCounts(object _, object __)
{
runBackupCountsAgain = true;
if (updateCountsBw is not null)
return;
updateCountsBw = new System.ComponentModel.BackgroundWorker();
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
updateCountsBw.RunWorkerAsync();
}
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
while (runBackupCountsAgain)
{
runBackupCountsAgain = false;
var libraryStats = LibraryCommands.GetCounts();
e.Result = libraryStats;
}
updateCountsBw = null;
}
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
{
var libraryStats = e.Result as LibraryCommands.LibraryStats;
setBookBackupCounts(libraryStats);
setPdfBackupCounts(libraryStats);
}
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
// enable/disable export
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
exportLibraryToolStripMenuItem.Enabled = hasResults;
// update bottom numbers
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
var statusStripText
= !hasResults ? "No books. Begin by importing your library"
: libraryStats.booksError > 0 ? string.Format(backupsCountsLbl_Format + " Errors: {3}", libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp, libraryStats.booksError)
: pending > 0 ? string.Format(backupsCountsLbl_Format, libraryStats.booksNoProgress, libraryStats.booksDownloadedOnly, libraryStats.booksFullyBackedUp)
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
// update menu item
var menuItemText
= pending > 0
? $"{pending} remaining"
: "All books have been liberated";
// update UI
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Enabled = pending > 0);
menuStrip1.UIThreadAsync(() => beginBookBackupsToolStripMenuItem.Text = string.Format(beginBookBackupsToolStripMenuItem_format, menuItemText));
}
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
{
var pdfsCountsLbl_Format = "| PDFs: NOT d/l\'ed: {0} Downloaded: {1}";
// update bottom numbers
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
var statusStripText
= !hasResults ? ""
: libraryStats.pdfsNotDownloaded > 0 ? string.Format(pdfsCountsLbl_Format, libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
// update menu item
var menuItemText
= libraryStats.pdfsNotDownloaded > 0
? $"{libraryStats.pdfsNotDownloaded} remaining"
: "All PDFs have been downloaded";
// update UI
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0);
menuStrip1.UIThreadAsync(() => beginPdfBackupsToolStripMenuItem.Text = string.Format(beginPdfBackupsToolStripMenuItem_format, menuItemText));
}
#endregion
#region filter
private void filterHelpBtn_Click(object sender, EventArgs e) => new SearchSyntaxDialog().ShowDialog();
private void AddFilterBtn_Click(object sender, EventArgs e) => QuickFilters.Add(this.filterSearchTb.Text);
private void filterSearchTb_KeyPress(object sender, KeyPressEventArgs e)
{
if (e.KeyChar == (char)Keys.Return)
{
doFilter();
// silence the 'ding'
e.Handled = true;
}
}
private void filterBtn_Click(object sender, EventArgs e) => doFilter();
private string lastGoodFilter = "";
private void doFilter(string filterString)
{
this.filterSearchTb.Text = filterString;
doFilter();
}
private void doFilter()
{
try
{
productsGrid.Filter(filterSearchTb.Text);
lastGoodFilter = filterSearchTb.Text;
}
catch (Exception ex)
{
MessageBox.Show($"Bad filter string:\r\n\r\n{ex.Message}", "Bad filter string", MessageBoxButtons.OK, MessageBoxIcon.Error);
// re-apply last good filter
doFilter(lastGoodFilter);
}
}
#endregion
#region Auto-scanner
private InterruptableTimer autoScanTimer;
private void configAndInitAutoScan()
{
var hours = 0;
var minutes = 5;
var seconds = 0;
var _5_minutes = new TimeSpan(hours, minutes, seconds);
autoScanTimer = new InterruptableTimer(_5_minutes);
// subscribe as async/non-blocking. I'd actually rather prefer blocking but real-world testing found that caused a deadlock in the AudibleAPI
autoScanTimer.Elapsed += async (_, __) =>
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.ToArray();
// in autoScan, new books SHALL NOT show dialog
await Invoke(async () => await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts));
};
// load init state to menu checkbox
this.Load += updateAutoScanLibraryToolStripMenuItem;
// if enabled: begin on load
this.Load += startAutoScan;
// if new 'default' account is added, run autoscan
AccountsSettingsPersister.Saving += accountsPreSave;
AccountsSettingsPersister.Saved += accountsPostSave;
// when autoscan setting is changed, update menu checkbox and run autoscan
Configuration.Instance.AutoScanChanged += updateAutoScanLibraryToolStripMenuItem;
Configuration.Instance.AutoScanChanged += startAutoScan;
}
private List<(string AccountId, string LocaleName)> preSaveDefaultAccounts;
private List<(string AccountId, string LocaleName)> getDefaultAccounts()
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
return persister.AccountsSettings
.GetAll()
.Where(a => a.LibraryScan)
.Select(a => (a.AccountId, a.Locale.Name))
.ToList();
}
private void accountsPreSave(object sender = null, EventArgs e = null)
=> preSaveDefaultAccounts = getDefaultAccounts();
private void accountsPostSave(object sender = null, EventArgs e = null)
{
var postSaveDefaultAccounts = getDefaultAccounts();
var newDefaultAccounts = postSaveDefaultAccounts.Except(preSaveDefaultAccounts).ToList();
if (newDefaultAccounts.Any())
startAutoScan();
}
private void startAutoScan(object sender = null, EventArgs e = null)
{
if (Configuration.Instance.AutoScan)
autoScanTimer.PerformNow();
else
autoScanTimer.Stop();
}
private void updateAutoScanLibraryToolStripMenuItem(object sender, EventArgs e) => autoScanLibraryToolStripMenuItem.Checked = Configuration.Instance.AutoScan;
#endregion
#region Import menu
private void refreshImportMenu(object _ = null, EventArgs __ = null)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var count = persister.AccountsSettings.Accounts.Count;
autoScanLibraryToolStripMenuItem.Visible = count > 0;
noAccountsYetAddAccountToolStripMenuItem.Visible = count == 0;
scanLibraryToolStripMenuItem.Visible = count == 1;
scanLibraryOfAllAccountsToolStripMenuItem.Visible = count > 1;
scanLibraryOfSomeAccountsToolStripMenuItem.Visible = count > 1;
removeLibraryBooksToolStripMenuItem.Visible = count > 0;
removeSomeAccountsToolStripMenuItem.Visible = count > 1;
removeAllAccountsToolStripMenuItem.Visible = count > 1;
}
private void autoScanLibraryToolStripMenuItem_Click(object sender, EventArgs e) => Configuration.Instance.AutoScan = !autoScanLibraryToolStripMenuItem.Checked;
private void noAccountsYetAddAccountToolStripMenuItem_Click(object sender, EventArgs e)
{
MessageBox.Show("To load your Audible library, come back here to the Import menu after adding your account");
new AccountsDialog(this).ShowDialog();
}
private async void scanLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var firstAccount = persister.AccountsSettings.GetAll().FirstOrDefault();
await scanLibrariesAsync(firstAccount);
}
private async void scanLibraryOfAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
await scanLibrariesAsync(allAccounts);
}
private async void scanLibraryOfSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
await scanLibrariesAsync(scanAccountsDialog.CheckedAccounts);
}
private void removeLibraryBooksToolStripMenuItem_Click(object sender, EventArgs e)
{
// if 0 accounts, this will not be visible
// if 1 account, run scanLibrariesRemovedBooks() on this account
// if multiple accounts, another menu set will open. do not run scanLibrariesRemovedBooks()
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var accounts = persister.AccountsSettings.GetAll();
if (accounts.Count != 1)
return;
var firstAccount = accounts.Single();
scanLibrariesRemovedBooks(firstAccount);
}
// selectively remove books from all accounts
private void removeAllAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var allAccounts = persister.AccountsSettings.GetAll();
scanLibrariesRemovedBooks(allAccounts.ToArray());
}
// selectively remove books from some accounts
private void removeSomeAccountsToolStripMenuItem_Click(object sender, EventArgs e)
{
using var scanAccountsDialog = new ScanAccountsDialog(this);
if (scanAccountsDialog.ShowDialog() != DialogResult.OK)
return;
if (!scanAccountsDialog.CheckedAccounts.Any())
return;
scanLibrariesRemovedBooks(scanAccountsDialog.CheckedAccounts.ToArray());
}
private void scanLibrariesRemovedBooks(params Account[] accounts)
{
using var dialog = new RemoveBooksDialog(accounts);
dialog.ShowDialog();
}
private async Task scanLibrariesAsync(IEnumerable<Account> accounts) => await scanLibrariesAsync(accounts.ToArray());
private async Task scanLibrariesAsync(params Account[] accounts)
{
try
{
var (totalProcessed, newAdded) = await LibraryCommands.ImportAccountAsync(Login.WinformLoginChoiceEager.ApiExtendedFunc, accounts);
// this is here instead of ScanEnd so that the following is only possible when it's user-initiated, not automatic loop
if (Configuration.Instance.ShowImportedStats && newAdded > 0)
MessageBox.Show($"Total processed: {totalProcessed}\r\nNew: {newAdded}");
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert(
"Error importing library. Please try again. If this still happens after 2 or 3 tries, stop and contact administrator",
"Error importing library",
ex);
}
}
#endregion
#region Liberate menu
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync();
private async void beginPdfBackupsToolStripMenuItem_Click(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllPdfsAsync();
private async void convertAllM4bToMp3ToolStripMenuItem_Click(object sender, EventArgs e)
{
var result = MessageBox.Show(
"This converts all m4b titles in your library to mp3 files. Original files are not deleted."
+ "\r\nFor large libraries this will take a long time and will take up more disk space."
+ "\r\n\r\nContinue?"
+ "\r\n\r\n(To always download titles as mp3 instead of m4b, go to Settings: Download my books as .MP3 files)",
"Convert all M4b => Mp3?",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
await BookLiberation.ProcessorAutomationController.ConvertAllBooksAsync();
}
#endregion
#region Export menu
private void exportLibraryToolStripMenuItem_Click(object sender, EventArgs e)
{
try
{
var saveFileDialog = new SaveFileDialog
{
Title = "Where to export Library",
Filter = "Excel Workbook (*.xlsx)|*.xlsx|CSV files (*.csv)|*.csv|JSON files (*.json)|*.json" // + "|All files (*.*)|*.*"
};
if (saveFileDialog.ShowDialog() != DialogResult.OK)
return;
// FilterIndex is 1-based, NOT 0-based
switch (saveFileDialog.FilterIndex)
{
case 1: // xlsx
default:
LibraryExporter.ToXlsx(saveFileDialog.FileName);
break;
case 2: // csv
LibraryExporter.ToCsv(saveFileDialog.FileName);
break;
case 3: // json
LibraryExporter.ToJson(saveFileDialog.FileName);
break;
}
MessageBox.Show("Library exported to:\r\n" + saveFileDialog.FileName);
}
catch (Exception ex)
{
MessageBoxLib.ShowAdminAlert("Error attempting to export your library.", "Error exporting", ex);
}
}
#endregion
#region Quick Filters menu
private void FirstFilterIsDefaultToolStripMenuItem_Click(object sender, EventArgs e)
{
firstFilterIsDefaultToolStripMenuItem.Checked = !firstFilterIsDefaultToolStripMenuItem.Checked;
QuickFilters.UseDefault = firstFilterIsDefaultToolStripMenuItem.Checked;
}
private void loadInitialQuickFilterState()
{
// set inital state. do once only
firstFilterIsDefaultToolStripMenuItem.Checked = QuickFilters.UseDefault;
// load default filter. do once only
if (QuickFilters.UseDefault)
doFilter(QuickFilters.Filters.FirstOrDefault());
updateFiltersMenu();
}
private object quickFilterTag { get; } = new object();
private void updateFiltersMenu(object _ = null, object __ = null)
{
// remove old
for (var i = quickFiltersToolStripMenuItem.DropDownItems.Count - 1; i >= 0; i--)
{
var menuItem = quickFiltersToolStripMenuItem.DropDownItems[i];
if (menuItem.Tag == quickFilterTag)
quickFiltersToolStripMenuItem.DropDownItems.Remove(menuItem);
}
// re-populate
var index = 0;
foreach (var filter in QuickFilters.Filters)
{
var menuItem = new ToolStripMenuItem
{
Tag = quickFilterTag,
Text = $"&{++index}: {filter}"
};
menuItem.Click += (_, __) => doFilter(filter);
quickFiltersToolStripMenuItem.DropDownItems.Add(menuItem);
}
}
private void EditQuickFiltersToolStripMenuItem_Click(object sender, EventArgs e) => new EditQuickFilters(this).ShowDialog();
#endregion
#region Visible Books menu
private void configVisibleBooksMenu()
{
productsGrid.VisibleCountChanged += (_, qty) => {
visibleBooksToolStripMenuItem.Text = string.Format(visibleBooksToolStripMenuItem_format, qty);
visibleBooksToolStripMenuItem.Enabled = qty > 0;
var notLiberatedCount = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
};
productsGrid.VisibleCountChanged += setLiberatedVisibleMenuItemAsync;
LibraryCommands.BookUserDefinedItemCommitted += setLiberatedVisibleMenuItemAsync;
}
private async void setLiberatedVisibleMenuItemAsync(object _, int __)
=> await Task.Run(setLiberatedVisibleMenuItem);
private async void setLiberatedVisibleMenuItemAsync(object _, EventArgs __)
=> await Task.Run(setLiberatedVisibleMenuItem);
void setLiberatedVisibleMenuItem()
{
var notLiberated = productsGrid.GetVisible().Count(lb => lb.Book.UserDefinedItem.BookStatus == DataLayer.LiberatedStatus.NotLiberated);
this.UIThreadSync(() =>
{
if (notLiberated > 0)
{
liberateVisibleToolStripMenuItem.Text = string.Format(liberateVisibleToolStripMenuItem_format, notLiberated);
liberateVisibleToolStripMenuItem.Enabled = true;
liberateVisible2ToolStripMenuItem.Text = string.Format(liberateVisible2ToolStripMenuItem_format, notLiberated);
liberateVisible2ToolStripMenuItem.Enabled = true;
}
else
{
liberateVisibleToolStripMenuItem.Text = "All visible books are liberated";
liberateVisibleToolStripMenuItem.Enabled = false;
liberateVisible2ToolStripMenuItem.Text = "All visible books are liberated";
liberateVisible2ToolStripMenuItem.Enabled = false;
}
});
}
private async void liberateVisible(object sender, EventArgs e)
=> await BookLiberation.ProcessorAutomationController.BackupAllBooksAsync(productsGrid.GetVisible());
private void replaceTagsToolStripMenuItem_Click(object sender, EventArgs e)
{
var dialog = new TagsBatchDialog();
var result = dialog.ShowDialog();
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = productsGrid.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace tags in {0}?",
"Replace tags?");
if (confirmationResult != DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.Tags = dialog.NewTags;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
private void setDownloadedToolStripMenuItem_Click(object sender, EventArgs e)
{
var dialog = new LiberatedStatusBatchDialog();
var result = dialog.ShowDialog();
if (result != DialogResult.OK)
return;
var visibleLibraryBooks = productsGrid.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to replace downloaded status in {0}?",
"Replace downloaded status?");
if (confirmationResult != DialogResult.Yes)
return;
foreach (var libraryBook in visibleLibraryBooks)
libraryBook.Book.UserDefinedItem.BookStatus = dialog.BookLiberatedStatus;
LibraryCommands.UpdateUserDefinedItem(visibleLibraryBooks.Select(lb => lb.Book));
}
private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
{
var visibleLibraryBooks = productsGrid.GetVisible();
var confirmationResult = MessageBoxLib.ShowConfirmationDialog(
visibleLibraryBooks,
$"Are you sure you want to remove {0} from Libation's library?",
"Remove books from Libation?");
if (confirmationResult != DialogResult.Yes)
return;
var visibleIds = visibleLibraryBooks.Select(lb => lb.Book.AudibleProductId).ToList();
await LibraryCommands.RemoveBooksAsync(visibleIds);
}
#endregion
#region Settings menu
private void accountsToolStripMenuItem_Click(object sender, EventArgs e) => new AccountsDialog(this).ShowDialog();
private void basicSettingsToolStripMenuItem_Click(object sender, EventArgs e) => new SettingsDialog().ShowDialog();
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
=> MessageBox.Show($"Running Libation version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
#endregion
#region Scanning label
private void LibraryCommands_ScanBegin(object sender, int accountsLength)
{
scanLibraryToolStripMenuItem.Enabled = false;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = false;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = false;
this.scanningToolStripMenuItem.Visible = true;
this.scanningToolStripMenuItem.Text
= (accountsLength == 1)
? "Scanning..."
: $"Scanning {accountsLength} accounts...";
}
private void LibraryCommands_ScanEnd(object sender, EventArgs e)
{
scanLibraryToolStripMenuItem.Enabled = true;
scanLibraryOfAllAccountsToolStripMenuItem.Enabled = true;
scanLibraryOfSomeAccountsToolStripMenuItem.Enabled = true;
this.scanningToolStripMenuItem.Visible = false;
}
#endregion
}
}
}

View File

@@ -57,12 +57,48 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="filterHelpBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="filterBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="filterSearchTb.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="menuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="menuStrip1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="statusStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>132, 17</value>
</metadata>
<metadata name="statusStrip1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="addQuickFilterBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="splitContainer1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="panel1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="productsDisplay.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="toggleQueueHideBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="processBookQueue1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>

View File

@@ -0,0 +1,32 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public class FormattableLabel : Label
{
public string FormatText { get; set; }
/// <summary>Text set: first non-null, non-whitespace <see cref="Text"/> set is also saved as <see cref="FormatText"/></summary>
public override string Text
{
get => base.Text;
set
{
if (string.IsNullOrWhiteSpace(FormatText))
FormatText = value;
base.Text = value;
}
}
#region ctor.s
public FormattableLabel() : base() { }
#endregion
/// <summary>Replaces the format item in a specified string with the string representation of a corresponding object in a specified array. Returns <see cref="Text"/> for convenience.</summary>
/// <param name="args">An object array that contains zero or more objects to format.</param>
public string Format(params object[] args) => Text = string.Format(FormatText, args);
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public class FormattableToolStripMenuItem : ToolStripMenuItem
{
public string FormatText { get; set; }
/// <summary>Text set: first non-null, non-whitespace <see cref="Text"/> set is also saved as <see cref="FormatText"/></summary>
public override string Text
{
get => base.Text;
set
{
if (string.IsNullOrWhiteSpace(FormatText))
FormatText = value;
base.Text = value;
}
}
#region ctor.s
public FormattableToolStripMenuItem() : base() { }
public FormattableToolStripMenuItem(string text) : base(text) => FormatText = text;
public FormattableToolStripMenuItem(Image image) : base(image) { }
public FormattableToolStripMenuItem(string text, Image image) : base(text, image) => FormatText = text;
public FormattableToolStripMenuItem(string text, Image image, EventHandler onClick) : base(text, image, onClick) => FormatText = text;
public FormattableToolStripMenuItem(string text, Image image, params ToolStripItem[] dropDownItems) : base(text, image, dropDownItems) => FormatText = text;
public FormattableToolStripMenuItem(string text, Image image, EventHandler onClick, Keys shortcutKeys) : base(text, image, onClick, shortcutKeys) => FormatText = text;
public FormattableToolStripMenuItem(string text, Image image, EventHandler onClick, string name) : base(text, image, onClick, name) => FormatText = text;
#endregion
/// <summary>Replaces the format item in a specified string with the string representation of a corresponding object in a specified array. Returns <see cref="Text"/> for convenience.</summary>
/// <param name="args">An object array that contains zero or more objects to format.</param>
public string Format(params object[] args) => Text = string.Format(FormatText, args);
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
{
public class FormattableToolStripStatusLabel : ToolStripStatusLabel
{
public string FormatText { get; set; }
/// <summary>Text set: first non-null, non-whitespace <see cref="Text"/> set is also saved as <see cref="FormatText"/></summary>
public override string Text
{
get => base.Text;
set
{
if (string.IsNullOrWhiteSpace(FormatText))
FormatText = value;
base.Text = value;
}
}
#region ctor.s
public FormattableToolStripStatusLabel() : base() { }
public FormattableToolStripStatusLabel(string text) : base(text) => FormatText = text;
public FormattableToolStripStatusLabel(Image image) : base(image) { }
public FormattableToolStripStatusLabel(string text, Image image) : base(text, image) => FormatText = text;
public FormattableToolStripStatusLabel(string text, Image image, EventHandler onClick) : base(text, image, onClick) => FormatText = text;
public FormattableToolStripStatusLabel(string text, Image image, EventHandler onClick, string name) : base(text, image, onClick, name) => FormatText = text;
#endregion
/// <summary>Replaces the format item in a specified string with the string representation of a corresponding object in a specified array. Returns <see cref="Text"/> for convenience.</summary>
/// <param name="args">An object array that contains zero or more objects to format.</param>
public string Format(params object[] args) => Text = string.Format(FormatText, args);
}
}

View File

@@ -2,12 +2,15 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public abstract class AsyncNotifyPropertyChanged : SynchronizeInvoker, INotifyPropertyChanged
{
// see also notes in Libation/Source/_ARCHITECTURE NOTES.txt :: MVVM
public event PropertyChangedEventHandler PropertyChanged;
// per standard INotifyPropertyChanged pattern:
// https://docs.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-implement-property-change-notification
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> this.UIThreadAsync(() => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)));
}

View File

@@ -1,7 +1,7 @@
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public class DataGridViewImageButtonCell : DataGridViewButtonCell
{

View File

@@ -1,4 +1,4 @@
namespace LibationWinForms
namespace LibationWinForms.GridView
{
partial class DescriptionDisplay
{

View File

@@ -1,17 +1,15 @@
using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public partial class DescriptionDisplay : Form
{
private int borderThickness = 5;
public int BorderThickness
public int BorderThickness
{
get => borderThickness;
set

View File

@@ -1,7 +1,8 @@
using System.Drawing;
using Dinah.Core.Windows.Forms;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public class EditTagsDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
@@ -18,6 +19,12 @@ namespace LibationWinForms
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is SeriesEntry)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, DataGridViewPaintParts.Background | DataGridViewPaintParts.Border);
return;
}
var tagsString = (string)value;
var foreColor = tagsString?.Contains("hidden") == true ? HiddenForeColor : DataGridView.DefaultCellStyle.ForeColor;

View File

@@ -0,0 +1,113 @@
using DataLayer;
using Dinah.Core.DataBinding;
using Dinah.Core.Drawing;
using LibationFileManager;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
namespace LibationWinForms.GridView
{
public abstract class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
{
protected abstract Book Book { get; }
private Image _cover;
#region Model properties exposed to the view
public Image Cover
{
get => _cover;
protected set
{
_cover = value;
NotifyPropertyChanged();
}
}
public new bool InvokeRequired => base.InvokeRequired;
public abstract DateTime DateAdded { get; }
public abstract float SeriesIndex { get; }
public abstract string ProductRating { get; protected set; }
public abstract string PurchaseDate { get; protected set; }
public abstract string MyRating { get; protected set; }
public abstract string Series { get; protected set; }
public abstract string Title { get; protected set; }
public abstract string Length { get; protected set; }
public abstract string Authors { get; protected set; }
public abstract string Narrators { get; protected set; }
public abstract string Category { get; protected set; }
public abstract string Misc { get; protected set; }
public abstract string Description { get; protected set; }
public abstract string DisplayTags { get; }
public abstract LiberateButtonStatus Liberate { get; }
#endregion
#region Sorting
public GridEntry() => _memberValues = CreateMemberValueDictionary();
private Dictionary<string, Func<object>> _memberValues { get; set; }
protected abstract Dictionary<string, Func<object>> CreateMemberValueDictionary();
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
// Used by GridEntryBindingList for all sorting
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
#endregion
protected void LoadCover()
{
// Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired.
_cover = ImageReader.ToImage(picture);
}
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == Book.PictureId)
{
Cover = ImageReader.ToImage(e.Picture);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
{
{ typeof(string), new ObjectComparer<string>() },
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(bool), new ObjectComparer<bool>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberateButtonStatus), new ObjectComparer<LiberateButtonStatus>() },
};
~GridEntry()
{
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
internal static class GridEntryExtensions
{
#nullable enable
public static IEnumerable<SeriesEntry> Series(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<SeriesEntry>();
public static IEnumerable<LibraryBookEntry> LibraryBooks(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.OfType<LibraryBookEntry>();
public static LibraryBookEntry? FindBookByAsin(this IEnumerable<LibraryBookEntry> gridEntries, string audibleProductID)
=> gridEntries.FirstOrDefault(i => i.AudibleProductId == audibleProductID);
public static SeriesEntry? FindBookSeriesEntry(this IEnumerable<GridEntry> gridEntries, IEnumerable<SeriesBook> matchSeries)
=> gridEntries.Series().FirstOrDefault(i => matchSeries.Any(s => s.Series.Name == i.Series));
public static IEnumerable<SeriesEntry> EmptySeries(this IEnumerable<GridEntry> gridEntries)
=> gridEntries.Series().Where(i => i.Children.Count == 0);
}
}

View File

@@ -0,0 +1,234 @@
using ApplicationServices;
using Dinah.Core.DataBinding;
using LibationSearchEngine;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace LibationWinForms.GridView
{
/*
* Allows filtering and sorting of the underlying BindingList<GridEntry>
* by implementing IBindingListView and using SearchEngineCommands
*
* When filtering is applied, the filtered-out items are removed
* from the base list and added to the private FilterRemoved list.
* When filtering is removed, items in the FilterRemoved list are
* added back to the base list.
*
* Remove is overridden to ensure that removed items are removed from
* the base list (visible items) as well as the FilterRemoved list.
*/
internal class GridEntryBindingList : BindingList<GridEntry>, IBindingListView
{
public GridEntryBindingList() : base(new List<GridEntry>()) { }
public GridEntryBindingList(IEnumerable<GridEntry> enumeration) : base(new List<GridEntry>(enumeration)) { }
/// <returns>All items in the list, including those filtered out.</returns>
public List<GridEntry> AllItems() => Items.Concat(FilterRemoved).ToList();
public bool SupportsFiltering => true;
public string Filter { get => FilterString; set => ApplyFilter(value); }
/// <summary>When true, itms will not be checked filtered by search criteria on item changed<summary>
public bool SuspendFilteringOnUpdate { get; set; }
protected MemberComparer<GridEntry> Comparer { get; } = new();
protected override bool SupportsSortingCore => true;
protected override bool SupportsSearchingCore => true;
protected override bool IsSortedCore => isSorted;
protected override PropertyDescriptor SortPropertyCore => propertyDescriptor;
protected override ListSortDirection SortDirectionCore => listSortDirection;
/// <summary> Items that were removed from the base list due to filtering </summary>
private readonly List<GridEntry> FilterRemoved = new();
private string FilterString;
private SearchResultSet SearchResults;
private bool isSorted;
private ListSortDirection listSortDirection;
private PropertyDescriptor propertyDescriptor;
#region Unused - Advanced Filtering
public bool SupportsAdvancedSorting => false;
//This ApplySort overload is only called if SupportsAdvancedSorting is true.
//Otherwise BindingList.ApplySort() is used
public void ApplySort(ListSortDescriptionCollection sorts) => throw new NotImplementedException();
public ListSortDescriptionCollection SortDescriptions => throw new NotImplementedException();
#endregion
public new void Remove(GridEntry entry)
{
FilterRemoved.Remove(entry);
base.Remove(entry);
}
private void ApplyFilter(string filterString)
{
if (filterString != FilterString)
RemoveFilter();
FilterString = filterString;
SearchResults = SearchEngineCommands.Search(filterString);
var booksFilteredIn = Items.LibraryBooks().Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => (GridEntry)lbe);
//Find all series containing children that match the search criteria
var seriesFilteredIn = Items.Series().Where(s => s.Children.Join(SearchResults.Docs, lbe => lbe.AudibleProductId, d => d.ProductId, (lbe, d) => lbe).Any());
var filteredOut = Items.Except(booksFilteredIn.Concat(seriesFilteredIn)).ToList();
foreach (var item in filteredOut)
{
FilterRemoved.Add(item);
base.Remove(item);
}
}
public void CollapseAll()
{
foreach (var series in Items.Series().ToList())
CollapseItem(series);
}
public void ExpandAll()
{
foreach (var series in Items.Series().ToList())
ExpandItem(series);
}
public void CollapseItem(SeriesEntry sEntry)
{
foreach (var episode in Items.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
{
FilterRemoved.Add(episode);
base.Remove(episode);
}
sEntry.Liberate.Expanded = false;
}
public void ExpandItem(SeriesEntry sEntry)
{
var sindex = Items.IndexOf(sEntry);
foreach (var episode in FilterRemoved.LibraryBooks().Where(b => b.Parent == sEntry).ToList())
{
if (SearchResults is null || SearchResults.Docs.Any(d => d.ProductId == episode.AudibleProductId))
{
FilterRemoved.Remove(episode);
InsertItem(++sindex, episode);
}
}
sEntry.Liberate.Expanded = true;
}
public void RemoveFilter()
{
if (FilterString is null) return;
int visibleCount = Items.Count;
foreach (var item in FilterRemoved.ToList())
{
if (item is SeriesEntry || (item is LibraryBookEntry lbe && (lbe.Parent is null || lbe.Parent.Liberate.Expanded)))
{
FilterRemoved.Remove(item);
InsertItem(visibleCount++, item);
}
}
if (IsSortedCore)
Sort();
else
//No user sort is applied, so do default sorting by DateAdded, descending
{
Comparer.PropertyName = nameof(GridEntry.DateAdded);
Comparer.Direction = ListSortDirection.Descending;
Sort();
}
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
FilterString = null;
SearchResults = null;
}
protected override void ApplySortCore(PropertyDescriptor property, ListSortDirection direction)
{
Comparer.PropertyName = property.Name;
Comparer.Direction = direction;
Sort();
propertyDescriptor = property;
listSortDirection = direction;
isSorted = true;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
protected void Sort()
{
var itemsList = (List<GridEntry>)Items;
var children = itemsList.LibraryBooks().Where(i => i.Parent is not null).ToList();
var sortedItems = itemsList.Except(children).OrderBy(ge => ge, Comparer).ToList();
itemsList.Clear();
//Only add parentless items at this stage. After these items are added in the
//correct sorting order, go back and add the children beneath their parents.
itemsList.AddRange(sortedItems);
foreach (var parent in children.Select(c => c.Parent).Distinct())
{
var pIndex = itemsList.IndexOf(parent);
//children should always be sorted by series index.
foreach (var c in children.Where(c => c.Parent == parent).OrderBy(c => c.SeriesIndex))
itemsList.Insert(++pIndex, c);
}
}
protected override void OnListChanged(ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.ItemChanged)
{
if (FilterString is not null && !SuspendFilteringOnUpdate && Items[e.NewIndex] is LibraryBookEntry lbItem)
{
SearchResults = SearchEngineCommands.Search(FilterString);
if (!SearchResults.Docs.Any(d => d.ProductId == lbItem.AudibleProductId))
{
FilterRemoved.Add(lbItem);
base.Remove(lbItem);
return;
}
}
if (isSorted && e.PropertyDescriptor == SortPropertyCore)
{
var item = Items[e.NewIndex];
Sort();
var newIndex = Items.IndexOf(item);
base.OnListChanged(new ListChangedEventArgs(ListChangedType.ItemMoved, newIndex, e.NewIndex));
return;
}
}
base.OnListChanged(e);
}
protected override void RemoveSortCore()
{
isSorted = false;
propertyDescriptor = base.SortPropertyCore;
listSortDirection = base.SortDirectionCore;
OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));
}
}
}

View File

@@ -1,4 +1,4 @@
namespace LibationWinForms
namespace LibationWinForms.GridView
{
partial class ImageDisplay
{

View File

@@ -1,11 +1,9 @@
using FileLiberator;
using LibationFileManager;
using System;
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public partial class ImageDisplay : Form
{

View File

@@ -0,0 +1,28 @@
using DataLayer;
using System;
namespace LibationWinForms.GridView
{
public class LiberateButtonStatus : IComparable
{
public LiberatedStatus BookStatus { get; set; }
public LiberatedStatus? PdfStatus { get; set; }
public bool Expanded { get; set; }
public bool IsSeries { get; init; }
/// <summary>
/// Defines the Liberate column's sorting behavior
/// </summary>
public int CompareTo(object obj)
{
if (obj is not LiberateButtonStatus second) return -1;
if (IsSeries && !second.IsSeries) return -1;
else if (!IsSeries && second.IsSeries) return 1;
else if (IsSeries && second.IsSeries) return 0;
else if (BookStatus == LiberatedStatus.Liberated && second.BookStatus != LiberatedStatus.Liberated) return -1;
else if (BookStatus != LiberatedStatus.Liberated && second.BookStatus == LiberatedStatus.Liberated) return 1;
else return BookStatus.CompareTo(second.BookStatus);
}
}
}

View File

@@ -1,10 +1,10 @@
using System;
using DataLayer;
using Dinah.Core.Windows.Forms;
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Linq;
using DataLayer;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
public class LiberateDataGridViewImageButtonColumn : DataGridViewButtonColumn
{
@@ -16,19 +16,32 @@ namespace LibationWinForms
internal class LiberateDataGridViewImageButtonCell : DataGridViewImageButtonCell
{
private static readonly Color SERIES_BG_COLOR = Color.FromArgb(230, 255, 230);
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object value, object formattedValue, string errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, null, null, null, cellStyle, advancedBorderStyle, paintParts);
if (value is (LiberatedStatus, LiberatedStatus) or (LiberatedStatus, null))
if (value is LiberateButtonStatus status)
{
var (bookState, pdfState) = ((LiberatedStatus bookState, LiberatedStatus? pdfState))value;
if (rowIndex >= 0 && DataGridView.GetBoundItem<GridEntry>(rowIndex) is LibraryBookEntry lbEntry && lbEntry.Parent is not null)
{
DataGridView.Rows[rowIndex].DefaultCellStyle.BackColor = SERIES_BG_COLOR;
}
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(bookState, pdfState);
if (status.IsSeries)
{
DrawButtonImage(graphics, status.Expanded ? Properties.Resources.minus: Properties.Resources.plus, cellBounds);
DrawButtonImage(graphics, buttonImage, cellBounds);
ToolTipText = status.Expanded ? "Click to Collpase" : "Click to Expand";
}
else
{
(string mouseoverText, Bitmap buttonImage) = GetLiberateDisplay(status.BookStatus, status.PdfStatus);
ToolTipText = mouseoverText;
DrawButtonImage(graphics, buttonImage, cellBounds);
ToolTipText = mouseoverText;
}
}
}

View File

@@ -1,23 +1,17 @@
using System;
using System.Collections;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using ApplicationServices;
using DataLayer;
using Dinah.Core.DataBinding;
using Dinah.Core;
using Dinah.Core.Drawing;
using LibationFileManager;
using System.Threading.Tasks;
namespace LibationWinForms
namespace LibationWinForms.GridView
{
/// <summary>
/// The View Model for a LibraryBook
/// </summary>
internal class GridEntry : AsyncNotifyPropertyChanged, IMemberComparable
public class LibraryBookEntry : GridEntry
{
#region implementation properties NOT exposed to the view
// hide from public fields from Data Source GUI with [Browsable(false)]
@@ -30,100 +24,67 @@ namespace LibationWinForms
public string LongDescription { get; private set; }
#endregion
protected override Book Book => LibraryBook.Book;
#region Model properties exposed to the view
private Image _cover;
private DateTime lastStatusUpdate = default;
private LiberatedStatus _bookStatus;
private LiberatedStatus? _pdfStatus;
public Image Cover
{
get => _cover;
private set
{
_cover = value;
NotifyPropertyChanged();
}
}
public bool DownloadInProgress { get; private set; }
public string ProductRating { get; private set; }
public string PurchaseDate { get; private set; }
public string MyRating { get; private set; }
public string Series { get; private set; }
public string Title { get; private set; }
public string Length { get; private set; }
public string Authors { get; private set; }
public string Narrators { get; private set; }
public string Category { get; private set; }
public string Misc { get; private set; }
public string Description { get; private set; }
public string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
public override DateTime DateAdded => LibraryBook.DateAdded;
public override float SeriesIndex => Book.SeriesLink.FirstOrDefault()?.Index ?? 0;
public override string ProductRating { get; protected set; }
public override string PurchaseDate { get; protected set; }
public override string MyRating { get; protected set; }
public override string Series { get; protected set; }
public override string Title { get; protected set; }
public override string Length { get; protected set; }
public override string Authors { get; protected set; }
public override string Narrators { get; protected set; }
public override string Category { get; protected set; }
public override string Misc { get; protected set; }
public override string Description { get; protected set; }
public override string DisplayTags => string.Join("\r\n", Book.UserDefinedItem.TagsEnumerated);
// these 2 values being in 1 field is the trick behind getting the liberated+pdf 'stoplight' icon to draw. See: LiberateDataGridViewImageButtonCell.Paint
public (LiberatedStatus BookStatus, LiberatedStatus? PdfStatus) Liberate
public override LiberateButtonStatus Liberate
{
get
{
//Cache these statuses for faster sorting.
if ((DateTime.Now - lastStatusUpdate).TotalSeconds > 2)
{
UpdateLiberatedStatus(notify: false);
_bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
lastStatusUpdate = DateTime.Now;
}
return (_bookStatus, _pdfStatus);
return new LiberateButtonStatus { BookStatus = _bookStatus, PdfStatus = _pdfStatus, IsSeries = false };
}
}
#endregion
public event EventHandler LibraryBookUpdated;
public event EventHandler Committed;
// alias
private Book Book => LibraryBook.Book;
public GridEntry(LibraryBook libraryBook) => setLibraryBook(libraryBook);
public async Task DownloadBook()
public LibraryBookEntry(LibraryBook libraryBook)
{
if (DownloadInProgress)
return;
try
{
DownloadInProgress = true;
await BookLiberation.ProcessorAutomationController.BackupSingleBookAsync(LibraryBook);
UpdateLiberatedStatus();
}
finally
{
DownloadInProgress = false;
}
setLibraryBook(libraryBook);
LoadCover();
}
public SeriesEntry Parent { get; init; }
public void UpdateLibraryBook(LibraryBook libraryBook)
{
if (AudibleProductId != libraryBook.Book.AudibleProductId)
throw new Exception("Invalid grid entry update. IDs must match");
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
setLibraryBook(libraryBook);
NotifyPropertyChanged();
}
private void setLibraryBook(LibraryBook libraryBook)
{
LibraryBook = libraryBook;
_memberValues = CreateMemberValueDictionary();
// Get cover art. If it's default, subscribe to PictureCached
{
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
// Mutable property. Set the field so PropertyChanged isn't fired.
_cover = ImageReader.ToImage(picture);
}
// Immutable properties
{
@@ -142,25 +103,14 @@ namespace LibationWinForms
}
UserDefinedItem.ItemChanged += UserDefinedItem_ItemChanged;
// this will never have a value when triggered by ctor b/c nothing can subscribe to the event until after ctor is complete
LibraryBookUpdated?.Invoke(this, null);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == Book.PictureId)
{
Cover = ImageReader.ToImage(e.Picture);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
#region detect changes to the model, update the view, and save to database.
/// <summary>
/// This event handler receives notifications from the model that it has changed.
/// Save to the database and notify the view that it's changed.
/// Notify the view that it's changed.
/// </summary>
private void UserDefinedItem_ItemChanged(object sender, string itemName)
{
@@ -169,6 +119,9 @@ namespace LibationWinForms
if (udi.Book.AudibleProductId != Book.AudibleProductId)
return;
// UDI changed, possibly in a different context/view. Update this viewmodel. Call NotifyPropertyChanged to notify view.
// - This method responds to tons of incidental changes. Do not persist to db from here. Committing to db must be a volitional action by the caller, not incidental. Otherwise batch changes would be impossible; we would only have slow one-offs
// - Don't restrict notifying view to 'only if property changed'. This same book instance can get passed to a different view, then changed there. When the chain of events makes its way back here, the property is unchanged (because it's the same instance), but this view is out of sync. NotifyPropertyChanged will then update this view.
switch (itemName)
{
case nameof(udi.Tags):
@@ -177,10 +130,12 @@ namespace LibationWinForms
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;
_bookStatus = udi.BookStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
case nameof(udi.PdfStatus):
Book.UserDefinedItem.PdfStatus = udi.PdfStatus;
_pdfStatus = udi.PdfStatus;
NotifyPropertyChanged(nameof(Liberate));
break;
}
@@ -188,49 +143,15 @@ namespace LibationWinForms
/// <summary>Save edits to the database</summary>
public void Commit(string newTags, LiberatedStatus bookStatus, LiberatedStatus? pdfStatus)
{
// validate
if (DisplayTags.EqualsInsensitive(newTags) &&
Liberate.BookStatus == bookStatus &&
Liberate.PdfStatus == pdfStatus)
return;
// update cache
_bookStatus = bookStatus;
_pdfStatus = pdfStatus;
// set + save
Book.UserDefinedItem.Tags = newTags;
Book.UserDefinedItem.BookStatus = bookStatus;
Book.UserDefinedItem.PdfStatus = pdfStatus;
LibraryCommands.UpdateUserDefinedItem(Book);
// notify
Committed?.Invoke(this, null);
}
private void UpdateLiberatedStatus(bool notify = true)
{
_bookStatus = LibraryCommands.Liberated_Status(LibraryBook.Book);
_pdfStatus = LibraryCommands.Pdf_Status(LibraryBook.Book);
if (notify)
NotifyPropertyChanged(nameof(Liberate));
}
// MVVM pass-through
=> Book.UpdateBook(newTags, bookStatus: bookStatus, pdfStatus: pdfStatus);
#endregion
#region Data Sorting
// These methods are implementation of Dinah.Core.DataBinding.IMemberComparable
// Used by Dinah.Core.DataBinding.SortableBindingList<T> for all sorting
public virtual object GetMemberValue(string memberName) => _memberValues[memberName]();
public virtual IComparer GetMemberComparer(Type memberType) => _memberTypeComparers[memberType];
private Dictionary<string, Func<object>> _memberValues { get; set; }
/// <summary>
/// Create getters for all member object values by name
/// </summary>
private Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
/// <summary>Create getters for all member object values by name </summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Title), () => Book.TitleSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
@@ -244,25 +165,17 @@ namespace LibationWinForms
{ nameof(Category), () => Category },
{ nameof(Misc), () => Misc },
{ nameof(DisplayTags), () => DisplayTags },
{ nameof(Liberate), () => Liberate.BookStatus }
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
// Instantiate comparers for every exposed member object type.
private static readonly Dictionary<Type, IComparer> _memberTypeComparers = new()
{
{ typeof(string), new ObjectComparer<string>() },
{ typeof(int), new ObjectComparer<int>() },
{ typeof(float), new ObjectComparer<float>() },
{ typeof(DateTime), new ObjectComparer<DateTime>() },
{ typeof(LiberatedStatus), new ObjectComparer<LiberatedStatus>() },
};
#endregion
#region Static library display functions
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
/// </summary>
private static string GetDescriptionDisplay(Book book)
{
@@ -280,7 +193,7 @@ namespace LibationWinForms
}
/// <summary>
/// This information should not change during <see cref="GridEntry"/> lifetime, so call only once.
/// This information should not change during <see cref="LibraryBookEntry"/> lifetime, so call only once.
/// Maximum of 5 text rows will fit in 80-pixel row height.
/// </summary>
private static string GetMiscDisplay(LibraryBook libraryBook)
@@ -310,10 +223,9 @@ namespace LibationWinForms
#endregion
~GridEntry()
~LibraryBookEntry()
{
UserDefinedItem.ItemChanged -= UserDefinedItem_ItemChanged;
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
}

View File

@@ -0,0 +1,64 @@
namespace LibationWinForms.GridView
{
partial class ProductsDisplay
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.productsGrid = new LibationWinForms.GridView.ProductsGrid();
this.SuspendLayout();
//
// productsGrid
//
this.productsGrid.AutoScroll = true;
this.productsGrid.Dock = System.Windows.Forms.DockStyle.Fill;
this.productsGrid.Location = new System.Drawing.Point(0, 0);
this.productsGrid.Name = "productsGrid";
this.productsGrid.Size = new System.Drawing.Size(1510, 380);
this.productsGrid.TabIndex = 0;
this.productsGrid.LiberateClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_LiberateClicked);
this.productsGrid.CoverClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_CoverClicked);
this.productsGrid.DetailsClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryClickedEventHandler(this.productsGrid_DetailsClicked);
this.productsGrid.DescriptionClicked += new LibationWinForms.GridView.ProductsGrid.LibraryBookEntryRectangleClickedEventHandler(this.productsGrid_DescriptionClicked);
this.productsGrid.VisibleCountChanged += new System.EventHandler<int>(this.productsGrid_VisibleCountChanged);
//
// ProductsDisplay
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.productsGrid);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "ProductsDisplay";
this.Size = new System.Drawing.Size(1510, 380);
this.ResumeLayout(false);
}
#endregion
private GridView.ProductsGrid productsGrid;
}
}

View File

@@ -0,0 +1,123 @@
using ApplicationServices;
using DataLayer;
using FileLiberator;
using LibationFileManager;
using LibationWinForms.Dialogs;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace LibationWinForms.GridView
{
public partial class ProductsDisplay : UserControl
{
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event EventHandler<LibraryBook> LiberateClicked;
public event EventHandler InitialLoaded;
private bool hasBeenDisplayed;
public ProductsDisplay()
{
InitializeComponent();
}
#region Button controls
private ImageDisplay imageDisplay;
private async void productsGrid_CoverClicked(LibraryBookEntry liveGridEntry)
{
var picDefinition = new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureLarge ?? liveGridEntry.LibraryBook.Book.PictureId, PictureSize.Native);
var picDlTask = Task.Run(() => PictureStorage.GetPictureSynchronously(picDefinition));
(_, byte[] initialImageBts) = PictureStorage.GetPicture(new PictureDefinition(liveGridEntry.LibraryBook.Book.PictureId, PictureSize._80x80));
var windowTitle = $"{liveGridEntry.Title} - Cover";
if (imageDisplay is null || imageDisplay.IsDisposed || !imageDisplay.Visible)
{
imageDisplay = new ImageDisplay();
imageDisplay.RestoreSizeAndLocation(Configuration.Instance);
imageDisplay.FormClosed += (_, _) => imageDisplay.SaveSizeAndLocation(Configuration.Instance);
imageDisplay.Show(this);
}
imageDisplay.BookSaveDirectory = AudibleFileStorage.Audio.GetDestinationDirectory(liveGridEntry.LibraryBook);
imageDisplay.PictureFileName = System.IO.Path.GetFileName(AudibleFileStorage.Audio.GetBooksDirectoryFilename(liveGridEntry.LibraryBook, ".jpg"));
imageDisplay.Text = windowTitle;
imageDisplay.CoverPicture = initialImageBts;
imageDisplay.CoverPicture = await picDlTask;
}
private void productsGrid_DescriptionClicked(LibraryBookEntry liveGridEntry, Rectangle cellRectangle)
{
var displayWindow = new DescriptionDisplay
{
SpawnLocation = PointToScreen(cellRectangle.Location + new Size(cellRectangle.Width, 0)),
DescriptionText = liveGridEntry.LongDescription,
BorderThickness = 2,
};
void CloseWindow(object o, EventArgs e)
{
displayWindow.Close();
}
productsGrid.Scroll += CloseWindow;
displayWindow.FormClosed += (_, _) => productsGrid.Scroll -= CloseWindow;
displayWindow.Show(this);
}
private void productsGrid_DetailsClicked(LibraryBookEntry liveGridEntry)
{
var bookDetailsForm = new BookDetailsDialog(liveGridEntry.LibraryBook);
if (bookDetailsForm.ShowDialog() == DialogResult.OK)
liveGridEntry.Commit(bookDetailsForm.NewTags, bookDetailsForm.BookLiberatedStatus, bookDetailsForm.PdfLiberatedStatus);
}
#endregion
#region UI display functions
public void Display()
{
// don't return early if lib size == 0. this will not update correctly if all books are removed
var lib = DbContexts.GetLibrary_Flat_NoTracking();
if (!hasBeenDisplayed)
{
// bind
productsGrid.BindToGrid(lib);
hasBeenDisplayed = true;
InitialLoaded?.Invoke(this, new());
}
else
productsGrid.UpdateGrid(lib);
}
#endregion
#region Filter
public void Filter(string searchString)
=> productsGrid.Filter(searchString);
#endregion
internal List<LibraryBook> GetVisible() => productsGrid.GetVisible().Select(v => v.LibraryBook).ToList();
private void productsGrid_VisibleCountChanged(object sender, int count)
{
VisibleCountChanged?.Invoke(this, count);
}
private void productsGrid_LiberateClicked(LibraryBookEntry liveGridEntry)
{
LiberateClicked?.Invoke(this, liveGridEntry.LibraryBook);
}
}
}

View File

@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@@ -58,4 +57,7 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>81</value>
</metadata>
</root>

View File

@@ -1,4 +1,4 @@
namespace LibationWinForms
namespace LibationWinForms.GridView
{
partial class ProductsGrid
{
@@ -29,10 +29,9 @@
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle1 = new System.Windows.Forms.DataGridViewCellStyle();
this.gridEntryBindingSource = new LibationWinForms.SyncBindingSource(this.components);
System.Windows.Forms.DataGridViewCellStyle dataGridViewCellStyle2 = new System.Windows.Forms.DataGridViewCellStyle();
this.gridEntryDataGridView = new System.Windows.Forms.DataGridView();
this.liberateGVColumn = new LibationWinForms.LiberateDataGridViewImageButtonColumn();
this.liberateGVColumn = new LibationWinForms.GridView.LiberateDataGridViewImageButtonColumn();
this.coverGVColumn = new System.Windows.Forms.DataGridViewImageColumn();
this.titleGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.authorsGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
@@ -45,16 +44,13 @@
this.purchaseDateGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.myRatingGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.miscGVColumn = new System.Windows.Forms.DataGridViewTextBoxColumn();
this.tagAndDetailsGVColumn = new LibationWinForms.EditTagsDataGridViewImageButtonColumn();
this.tagAndDetailsGVColumn = new LibationWinForms.GridView.EditTagsDataGridViewImageButtonColumn();
this.contextMenuStrip1 = new System.Windows.Forms.ContextMenuStrip(this.components);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).BeginInit();
this.syncBindingSource = new LibationWinForms.GridView.SyncBindingSource(this.components);
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).BeginInit();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).BeginInit();
this.SuspendLayout();
//
// gridEntryBindingSource
//
this.gridEntryBindingSource.DataSource = typeof(LibationWinForms.GridEntry);
//
// gridEntryDataGridView
//
this.gridEntryDataGridView.AllowUserToAddRows = false;
@@ -64,42 +60,40 @@
this.gridEntryDataGridView.AutoGenerateColumns = false;
this.gridEntryDataGridView.ColumnHeadersHeightSizeMode = System.Windows.Forms.DataGridViewColumnHeadersHeightSizeMode.AutoSize;
this.gridEntryDataGridView.Columns.AddRange(new System.Windows.Forms.DataGridViewColumn[] {
this.liberateGVColumn,
this.coverGVColumn,
this.titleGVColumn,
this.authorsGVColumn,
this.narratorsGVColumn,
this.lengthGVColumn,
this.seriesGVColumn,
this.descriptionGVColumn,
this.categoryGVColumn,
this.productRatingGVColumn,
this.purchaseDateGVColumn,
this.myRatingGVColumn,
this.miscGVColumn,
this.tagAndDetailsGVColumn});
this.liberateGVColumn,
this.coverGVColumn,
this.titleGVColumn,
this.authorsGVColumn,
this.narratorsGVColumn,
this.lengthGVColumn,
this.seriesGVColumn,
this.descriptionGVColumn,
this.categoryGVColumn,
this.productRatingGVColumn,
this.purchaseDateGVColumn,
this.myRatingGVColumn,
this.miscGVColumn,
this.tagAndDetailsGVColumn});
this.gridEntryDataGridView.ContextMenuStrip = this.contextMenuStrip1;
this.gridEntryDataGridView.DataSource = this.gridEntryBindingSource;
dataGridViewCellStyle1.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle1.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle1.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle1.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle1.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle1.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle1.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle1;
this.gridEntryDataGridView.DataSource = this.syncBindingSource;
dataGridViewCellStyle2.Alignment = System.Windows.Forms.DataGridViewContentAlignment.MiddleLeft;
dataGridViewCellStyle2.BackColor = System.Drawing.SystemColors.Window;
dataGridViewCellStyle2.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
dataGridViewCellStyle2.ForeColor = System.Drawing.SystemColors.ControlText;
dataGridViewCellStyle2.SelectionBackColor = System.Drawing.SystemColors.Highlight;
dataGridViewCellStyle2.SelectionForeColor = System.Drawing.SystemColors.HighlightText;
dataGridViewCellStyle2.WrapMode = System.Windows.Forms.DataGridViewTriState.True;
this.gridEntryDataGridView.DefaultCellStyle = dataGridViewCellStyle2;
this.gridEntryDataGridView.Dock = System.Windows.Forms.DockStyle.Fill;
this.gridEntryDataGridView.Location = new System.Drawing.Point(0, 0);
this.gridEntryDataGridView.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.gridEntryDataGridView.Name = "gridEntryDataGridView";
this.gridEntryDataGridView.ReadOnly = true;
this.gridEntryDataGridView.RowHeadersVisible = false;
this.gridEntryDataGridView.RowTemplate.Height = 82;
this.gridEntryDataGridView.Size = new System.Drawing.Size(1510, 380);
this.gridEntryDataGridView.TabIndex = 0;
this.gridEntryDataGridView.CellContentClick += new System.Windows.Forms.DataGridViewCellEventHandler(this.DataGridView_CellContentClick);
this.gridEntryDataGridView.CellToolTipTextNeeded += new System.Windows.Forms.DataGridViewCellToolTipTextNeededEventHandler(this.gridEntryDataGridView_CellToolTipTextNeeded);
this.gridEntryDataGridView.ColumnDisplayIndexChanged += new System.Windows.Forms.DataGridViewColumnEventHandler(this.gridEntryDataGridView_ColumnDisplayIndexChanged);
this.gridEntryDataGridView.ColumnWidthChanged += new System.Windows.Forms.DataGridViewColumnEventHandler(this.gridEntryDataGridView_ColumnWidthChanged);
//
// liberateGVColumn
//
@@ -218,23 +212,27 @@
this.contextMenuStrip1.Name = "contextMenuStrip1";
this.contextMenuStrip1.Size = new System.Drawing.Size(61, 4);
//
// syncBindingSource
//
this.syncBindingSource.DataSource = typeof(LibationWinForms.GridView.GridEntry);
//
// ProductsGrid
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.AutoScroll = true;
this.Controls.Add(this.gridEntryDataGridView);
this.Margin = new System.Windows.Forms.Padding(4, 3, 4, 3);
this.Name = "ProductsGrid";
this.Size = new System.Drawing.Size(1510, 380);
((System.ComponentModel.ISupportInitialize)(this.gridEntryBindingSource)).EndInit();
this.Load += new System.EventHandler(this.ProductsGrid_Load);
((System.ComponentModel.ISupportInitialize)(this.gridEntryDataGridView)).EndInit();
((System.ComponentModel.ISupportInitialize)(this.syncBindingSource)).EndInit();
this.ResumeLayout(false);
}
#endregion
private LibationWinForms.SyncBindingSource gridEntryBindingSource;
#endregion
private System.Windows.Forms.DataGridView gridEntryDataGridView;
private System.Windows.Forms.ContextMenuStrip contextMenuStrip1;
private LiberateDataGridViewImageButtonColumn liberateGVColumn;
@@ -251,5 +249,6 @@
private System.Windows.Forms.DataGridViewTextBoxColumn myRatingGVColumn;
private System.Windows.Forms.DataGridViewTextBoxColumn miscGVColumn;
private EditTagsDataGridViewImageButtonColumn tagAndDetailsGVColumn;
private SyncBindingSource syncBindingSource;
}
}

View File

@@ -0,0 +1,323 @@
using DataLayer;
using Dinah.Core.Windows.Forms;
using LibationFileManager;
using System;
using System.Collections.Generic;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace LibationWinForms.GridView
{
public partial class ProductsGrid : UserControl
{
public delegate void LibraryBookEntryClickedEventHandler(LibraryBookEntry liveGridEntry);
public delegate void LibraryBookEntryRectangleClickedEventHandler(LibraryBookEntry liveGridEntry, Rectangle cellRectangle);
/// <summary>Number of visible rows has changed</summary>
public event EventHandler<int> VisibleCountChanged;
public event LibraryBookEntryClickedEventHandler LiberateClicked;
public event LibraryBookEntryClickedEventHandler CoverClicked;
public event LibraryBookEntryClickedEventHandler DetailsClicked;
public event LibraryBookEntryRectangleClickedEventHandler DescriptionClicked;
public new event EventHandler<ScrollEventArgs> Scroll;
private GridEntryBindingList bindingList;
internal IEnumerable<LibraryBookEntry> GetVisible()
=> bindingList
.LibraryBooks();
public ProductsGrid()
{
InitializeComponent();
EnableDoubleBuffering();
gridEntryDataGridView.Scroll += (_, s) => Scroll?.Invoke(this, s);
}
private void EnableDoubleBuffering()
{
var propertyInfo = gridEntryDataGridView.GetType().GetProperty("DoubleBuffered", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
propertyInfo.SetValue(gridEntryDataGridView, true, null);
}
#region Button controls
private void DataGridView_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
// handle grid button click: https://stackoverflow.com/a/13687844
if (e.RowIndex < 0)
return;
var entry = getGridEntry(e.RowIndex);
if (entry is LibraryBookEntry lbEntry)
{
if (e.ColumnIndex == liberateGVColumn.Index)
LiberateClicked?.Invoke(lbEntry);
else if (e.ColumnIndex == tagAndDetailsGVColumn.Index)
DetailsClicked?.Invoke(lbEntry);
else if (e.ColumnIndex == descriptionGVColumn.Index)
DescriptionClicked?.Invoke(lbEntry, gridEntryDataGridView.GetCellDisplayRectangle(e.ColumnIndex, e.RowIndex, false));
else if (e.ColumnIndex == coverGVColumn.Index)
CoverClicked?.Invoke(lbEntry);
}
else if (entry is SeriesEntry sEntry && e.ColumnIndex == liberateGVColumn.Index)
{
if (sEntry.Liberate.Expanded)
bindingList.CollapseItem(sEntry);
else
bindingList.ExpandItem(sEntry);
sEntry.NotifyPropertyChanged(nameof(sEntry.Liberate));
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
}
}
private GridEntry getGridEntry(int rowIndex) => gridEntryDataGridView.GetBoundItem<GridEntry>(rowIndex);
#endregion
#region UI display functions
internal void BindToGrid(List<LibraryBook> dbBooks)
{
var geList = dbBooks.Where(b => b.Book.ContentType is not ContentType.Episode).Select(b => new LibraryBookEntry(b)).Cast<GridEntry>().ToList();
var episodes = dbBooks.Where(b => b.Book.ContentType is ContentType.Episode).ToList();
foreach (var series in episodes.Select(lb => lb.Book.SeriesLink.First()).DistinctBy(s => s.Series))
{
var seriesEntry = new SeriesEntry(series, episodes.Where(lb => lb.Book.SeriesLink.First().Series == series.Book.SeriesLink.First().Series));
geList.Add(seriesEntry);
geList.AddRange(seriesEntry.Children);
}
bindingList = new GridEntryBindingList(geList.OrderByDescending(e => e.DateAdded));
bindingList.CollapseAll();
syncBindingSource.DataSource = bindingList;
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
}
internal void UpdateGrid(List<LibraryBook> dbBooks)
{
string existingFilter = syncBindingSource.Filter;
Filter(null);
bindingList.SuspendFilteringOnUpdate = true;
//Add absent books to grid, or update current books
var allItmes = bindingList.AllItems().LibraryBooks();
foreach (var libraryBook in dbBooks)
{
var existingItem = allItmes.FindBookByAsin(libraryBook.Book.AudibleProductId);
// add new to top
if (existingItem is null)
{
if (libraryBook.Book.ContentType is ContentType.Episode)
{
LibraryBookEntry lbe;
//Find the series that libraryBook belongs to, if it exists
var series = bindingList.AllItems().FindBookSeriesEntry(libraryBook.Book.SeriesLink);
if (series is null)
{
//Series doesn't exist yet, so create and add it
var newSeries = new SeriesEntry(libraryBook.Book.SeriesLink.First(), libraryBook);
lbe = newSeries.Children[0];
newSeries.Liberate.Expanded = true;
bindingList.Insert(0, newSeries);
series = newSeries;
}
else
{
lbe = new(libraryBook) { Parent = series };
series.Children.Add(lbe);
}
//Add episode beneath the parent
int seriesIndex = bindingList.IndexOf(series);
bindingList.Insert(seriesIndex + 1, lbe);
if (series.Liberate.Expanded)
bindingList.ExpandItem(series);
else
bindingList.CollapseItem(series);
series.NotifyPropertyChanged();
}
else
//Add the new product
bindingList.Insert(0, new LibraryBookEntry(libraryBook));
}
// update existing
else
{
existingItem.UpdateLibraryBook(libraryBook);
}
}
bindingList.SuspendFilteringOnUpdate = false;
//Re-filter after updating existing / adding new books to capture any changes
Filter(existingFilter);
// remove deleted from grid.
// note: actual deletion from db must still occur via the RemoveBook feature. deleting from audible will not trigger this
var removedBooks =
bindingList
.AllItems()
.LibraryBooks()
.ExceptBy(dbBooks.Select(lb => lb.Book.AudibleProductId), ge => ge.AudibleProductId);
//Remove books in series from their parents' Children list
foreach (var removed in removedBooks.Where(b => b.Parent is not null))
{
removed.Parent.Children.Remove(removed);
removed.Parent.NotifyPropertyChanged();
}
//Remove series that have no children
var removedSeries =
bindingList
.AllItems()
.EmptySeries();
foreach (var removed in removedBooks.Cast<GridEntry>().Concat(removedSeries))
//no need to re-filter for removed books
bindingList.Remove(removed);
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
}
#endregion
#region Filter
public void Filter(string searchString)
{
int visibleCount = bindingList.Count;
if (string.IsNullOrEmpty(searchString))
syncBindingSource.RemoveFilter();
else
syncBindingSource.Filter = searchString;
if (visibleCount != bindingList.Count)
VisibleCountChanged?.Invoke(this, bindingList.LibraryBooks().Count());
}
#endregion
#region Column Customizations
private void ProductsGrid_Load(object sender, EventArgs e)
{
//https://stackoverflow.com/a/4498512/3335599
if (System.ComponentModel.LicenseManager.UsageMode == System.ComponentModel.LicenseUsageMode.Designtime) return;
gridEntryDataGridView.ColumnWidthChanged += gridEntryDataGridView_ColumnWidthChanged;
gridEntryDataGridView.ColumnDisplayIndexChanged += gridEntryDataGridView_ColumnDisplayIndexChanged;
contextMenuStrip1.Items.Add(new ToolStripLabel("Show / Hide Columns"));
contextMenuStrip1.Items.Add(new ToolStripSeparator());
//Restore Grid Display Settings
var config = Configuration.Instance;
var gridColumnsVisibilities = config.GridColumnsVisibilities;
var gridColumnsWidths = config.GridColumnsWidths;
var displayIndices = config.GridColumnsDisplayIndices;
var cmsKiller = new ContextMenuStrip();
foreach (DataGridViewColumn column in gridEntryDataGridView.Columns)
{
var itemName = column.DataPropertyName;
var visible = gridColumnsVisibilities.GetValueOrDefault(itemName, true);
var menuItem = new ToolStripMenuItem()
{
Text = column.HeaderText,
Checked = visible,
Tag = itemName
};
menuItem.Click += HideMenuItem_Click;
contextMenuStrip1.Items.Add(menuItem);
column.Width = gridColumnsWidths.GetValueOrDefault(itemName, column.Width);
column.MinimumWidth = 10;
column.HeaderCell.ContextMenuStrip = contextMenuStrip1;
column.Visible = visible;
//Setting a default ContextMenuStrip will allow the columns to handle the
//Show() event so it is not passed up to the _dataGridView.ContextMenuStrip.
//This allows the ContextMenuStrip to be shown if right-clicking in the gray
//background of _dataGridView but not shown if right-clicking inside cells.
column.ContextMenuStrip = cmsKiller;
}
//We must set DisplayIndex properties in ascending order
foreach (var itemName in displayIndices.OrderBy(i => i.Value).Select(i => i.Key))
{
var column = gridEntryDataGridView.Columns
.Cast<DataGridViewColumn>()
.Single(c => c.DataPropertyName == itemName);
column.DisplayIndex = displayIndices.GetValueOrDefault(itemName, column.Index);
}
}
private void HideMenuItem_Click(object sender, EventArgs e)
{
var menuItem = sender as ToolStripMenuItem;
var propertyName = menuItem.Tag as string;
var column = gridEntryDataGridView.Columns
.Cast<DataGridViewColumn>()
.FirstOrDefault(c => c.DataPropertyName == propertyName);
if (column != null)
{
var visible = menuItem.Checked;
menuItem.Checked = !visible;
column.Visible = !visible;
var config = Configuration.Instance;
var dictionary = config.GridColumnsVisibilities;
dictionary[propertyName] = column.Visible;
config.GridColumnsVisibilities = dictionary;
}
}
private void gridEntryDataGridView_ColumnDisplayIndexChanged(object sender, DataGridViewColumnEventArgs e)
{
var config = Configuration.Instance;
var dictionary = config.GridColumnsDisplayIndices;
dictionary[e.Column.DataPropertyName] = e.Column.DisplayIndex;
config.GridColumnsDisplayIndices = dictionary;
}
private void gridEntryDataGridView_CellToolTipTextNeeded(object sender, DataGridViewCellToolTipTextNeededEventArgs e)
{
if (e.ColumnIndex == descriptionGVColumn.Index)
e.ToolTipText = "Click to see full description";
else if (e.ColumnIndex == coverGVColumn.Index)
e.ToolTipText = "Click to see full size";
}
private void gridEntryDataGridView_ColumnWidthChanged(object sender, DataGridViewColumnEventArgs e)
{
var config = Configuration.Instance;
var dictionary = config.GridColumnsWidths;
dictionary[e.Column.DataPropertyName] = e.Column.Width;
}
#endregion
}
}

View File

@@ -57,13 +57,16 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="gridEntryBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<metadata name="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>171, 17</value>
</metadata>
<metadata name="syncBindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>17, 17</value>
</metadata>
<metadata name="contextMenuStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>197, 17</value>
<metadata name="bindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>326, 17</value>
</metadata>
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>81</value>
<metadata name="bindingSource.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>326, 17</value>
</metadata>
</root>

View File

@@ -0,0 +1,109 @@
using DataLayer;
using Dinah.Core;
using System;
using System.Collections.Generic;
using System.Linq;
namespace LibationWinForms.GridView
{
public class SeriesEntry : GridEntry
{
public List<LibraryBookEntry> Children { get; init; }
public override DateTime DateAdded => Children.Max(c => c.DateAdded);
public override float SeriesIndex { get; }
public override string ProductRating
{
get
{
var productAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.Rating.StoryRating));
return productAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
}
protected set => throw new NotImplementedException();
}
public override string PurchaseDate { get; protected set; }
public override string MyRating
{
get
{
var myAverageRating = new Rating(Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.OverallRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.PerformanceRating), Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.StoryRating));
return myAverageRating.ToStarString()?.DefaultIfNullOrWhiteSpace("");
}
protected set => throw new NotImplementedException();
}
public override string Series { get; protected set; }
public override string Title { get; protected set; }
public override string Length
{
get
{
int bookLenMins = Children.Sum(c => c.LibraryBook.Book.LengthInMinutes);
return bookLenMins == 0 ? "" : $"{bookLenMins / 60} hr {bookLenMins % 60} min";
}
protected set => throw new NotImplementedException();
}
public override string Authors { get; protected set; }
public override string Narrators { get; protected set; }
public override string Category { get; protected set; }
public override string Misc { get; protected set; } = string.Empty;
public override string Description { get; protected set; } = string.Empty;
public override string DisplayTags { get; } = string.Empty;
public override LiberateButtonStatus Liberate { get; }
protected override Book Book => SeriesBook.Book;
private SeriesBook SeriesBook { get; set; }
private SeriesEntry(SeriesBook seriesBook)
{
Liberate = new LiberateButtonStatus { IsSeries = true };
SeriesIndex = seriesBook.Index;
}
public SeriesEntry(SeriesBook seriesBook, IEnumerable<LibraryBook> children) : this(seriesBook)
{
Children = children.Select(c => new LibraryBookEntry(c) { Parent = this }).OrderBy(c => c.SeriesIndex).ToList();
SetSeriesBook(seriesBook);
}
public SeriesEntry(SeriesBook seriesBook, LibraryBook child) : this(seriesBook)
{
Children = new() { new LibraryBookEntry(child) { Parent = this } };
SetSeriesBook(seriesBook);
}
private void SetSeriesBook(SeriesBook seriesBook)
{
SeriesBook = seriesBook;
LoadCover();
// Immutable properties
{
Title = SeriesBook.Series.Name;
Series = SeriesBook.Series.Name;
PurchaseDate = Children.Min(c => c.LibraryBook.DateAdded).ToString("d");
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
}
}
/// <summary>Create getters for all member object values by name</summary>
protected override Dictionary<string, Func<object>> CreateMemberValueDictionary() => new()
{
{ nameof(Title), () => Book.SeriesSortable() },
{ nameof(Series), () => Book.SeriesSortable() },
{ nameof(Length), () => Children.Sum(c => c.LibraryBook.Book.LengthInMinutes) },
{ nameof(MyRating), () => Children.Average(c => c.LibraryBook.Book.UserDefinedItem.Rating.FirstScore()) },
{ nameof(PurchaseDate), () => Children.Min(c => c.LibraryBook.DateAdded) },
{ nameof(ProductRating), () => Children.Average(c => c.LibraryBook.Book.Rating.FirstScore()) },
{ nameof(Authors), () => string.Empty },
{ nameof(Narrators), () => string.Empty },
{ nameof(Description), () => string.Empty },
{ nameof(Category), () => string.Empty },
{ nameof(Misc), () => string.Empty },
{ nameof(DisplayTags), () => string.Empty },
{ nameof(Liberate), () => Liberate },
{ nameof(DateAdded), () => DateAdded },
};
}
}

View File

@@ -0,0 +1,29 @@
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
// https://stackoverflow.com/a/32886415
namespace LibationWinForms.GridView
{
public class SyncBindingSource : BindingSource
{
private SynchronizationContext syncContext { get; }
public SyncBindingSource() : base()
=> syncContext = SynchronizationContext.Current;
public SyncBindingSource(IContainer container) : base(container)
=> syncContext = SynchronizationContext.Current;
public SyncBindingSource(object dataSource, string dataMember) : base(dataSource, dataMember)
=> syncContext = SynchronizationContext.Current;
public override bool SupportsFiltering => true;
protected override void OnListChanged(ListChangedEventArgs e)
{
if (syncContext is not null)
syncContext.Send(_ => base.OnListChanged(e), null);
else
base.OnListChanged(e);
}
}
}

View File

@@ -28,7 +28,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.0.1" />
<PackageReference Include="Autoupdater.NET.Official" Version="1.7.0" />
<PackageReference Include="Dinah.Core.WindowsDesktop" Version="4.2.2.1" />
</ItemGroup>
<ItemGroup>
@@ -37,6 +38,12 @@
<ProjectReference Include="..\FileLiberator\FileLiberator.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Form1.*.cs">
<DependentUpon>Form1.cs</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\Resources.Designer.cs">
<DesignTime>True</DesignTime>

View File

@@ -0,0 +1,9 @@
using System;
namespace LibationWinForms.ProcessQueue
{
public interface ILogForm
{
void WriteLine(string text);
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LibationWinForms.ProcessQueue
{
// decouple serilog and form. include convenience factory method
public class LogMe
{
public event EventHandler<string> LogInfo;
public event EventHandler<string> LogErrorString;
public event EventHandler<(Exception, string)> LogError;
private LogMe()
{
LogInfo += (_, text) => Serilog.Log.Logger.Information($"Automated backup: {text}");
LogErrorString += (_, text) => Serilog.Log.Logger.Error(text);
LogError += (_, tuple) => Serilog.Log.Logger.Error(tuple.Item1, tuple.Item2 ?? "Automated backup: error");
}
private static ILogForm LogForm;
public static LogMe RegisterForm<T>(T form) where T : ILogForm
{
var logMe = new LogMe();
if (form is null)
return logMe;
LogForm = form;
logMe.LogInfo += LogMe_LogInfo;
logMe.LogErrorString += LogMe_LogErrorString;
logMe.LogError += LogMe_LogError;
return logMe;
}
private static async void LogMe_LogError(object sender, (Exception, string) tuple)
{
await Task.Run(() => LogForm?.WriteLine(tuple.Item2 ?? "Automated backup: error"));
await Task.Run(() => LogForm?.WriteLine("ERROR: " + tuple.Item1.Message));
}
private static async void LogMe_LogErrorString(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
private static async void LogMe_LogInfo(object sender, string text)
{
await Task.Run(() => LogForm?.WriteLine(text));
}
public void Info(string text) => LogInfo?.Invoke(this, text);
public void Error(string text) => LogErrorString?.Invoke(this, text);
public void Error(Exception ex, string text = null) => LogError?.Invoke(this, (ex, text));
}
}

View File

@@ -0,0 +1,380 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using ApplicationServices;
using DataLayer;
using Dinah.Core;
using FileLiberator;
using LibationFileManager;
namespace LibationWinForms.ProcessQueue
{
public enum ProcessBookResult
{
None,
Success,
Cancelled,
ValidationFail,
FailedRetry,
FailedSkip,
FailedAbort
}
public enum ProcessBookStatus
{
Queued,
Cancelled,
Working,
Completed,
Failed
}
/// <summary>
/// This is the viewmodel for queued processables
/// </summary>
public class ProcessBook : INotifyPropertyChanged
{
public event EventHandler Completed;
public event PropertyChangedEventHandler PropertyChanged;
private ProcessBookResult _result = ProcessBookResult.None;
private ProcessBookStatus _status = ProcessBookStatus.Queued;
private string _bookText;
private int _progress;
private TimeSpan _timeRemaining;
private Image _cover;
public ProcessBookResult Result { get => _result; private set { _result = value; NotifyPropertyChanged(); } }
public ProcessBookStatus Status { get => _status; private set { _status = value; NotifyPropertyChanged(); } }
public string BookText { get => _bookText; private set { _bookText = value; NotifyPropertyChanged(); } }
public int Progress { get => _progress; private set { _progress = value; NotifyPropertyChanged(); } }
public TimeSpan TimeRemaining { get => _timeRemaining; private set { _timeRemaining = value; NotifyPropertyChanged(); } }
public Image Cover { get => _cover; private set { _cover = value; NotifyPropertyChanged(); } }
public LibraryBook LibraryBook { get; private set; }
private Processable CurrentProcessable => _currentProcessable ??= Processes.Dequeue().Invoke();
private Processable NextProcessable() => _currentProcessable = null;
private Processable _currentProcessable;
private readonly Queue<Func<Processable>> Processes = new();
private readonly LogMe Logger;
public void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public ProcessBook(LibraryBook libraryBook, LogMe logme)
{
LibraryBook = libraryBook;
Logger = logme;
title = LibraryBook.Book.Title;
authorNames = LibraryBook.Book.AuthorNames();
narratorNames = LibraryBook.Book.NarratorNames();
_bookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
(bool isDefault, byte[] picture) = PictureStorage.GetPicture(new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._80x80));
if (isDefault)
PictureStorage.PictureCached += PictureStorage_PictureCached;
_cover = Dinah.Core.Drawing.ImageReader.ToImage(picture);
}
private void PictureStorage_PictureCached(object sender, PictureCachedEventArgs e)
{
if (e.Definition.PictureId == LibraryBook.Book.PictureId)
{
Cover = Dinah.Core.Drawing.ImageReader.ToImage(e.Picture);
PictureStorage.PictureCached -= PictureStorage_PictureCached;
}
}
public async Task<ProcessBookResult> ProcessOneAsync()
{
string procName = CurrentProcessable.Name;
try
{
LinkProcessable(CurrentProcessable);
var statusHandler = await CurrentProcessable.ProcessSingleAsync(LibraryBook, validate: true);
if (statusHandler.IsSuccess)
return Result = ProcessBookResult.Success;
else if (statusHandler.Errors.Contains("Cancelled"))
{
Logger.Info($"{procName}: Process was cancelled {LibraryBook.Book}");
return Result = ProcessBookResult.Cancelled;
}
else if (statusHandler.Errors.Contains("Validation failed"))
{
Logger.Info($"{procName}: Validation failed {LibraryBook.Book}");
return Result = ProcessBookResult.ValidationFail;
}
foreach (var errorMessage in statusHandler.Errors)
Logger.Error($"{procName}: {errorMessage}");
}
catch (Exception ex)
{
Logger.Error(ex, procName);
}
finally
{
if (Result == ProcessBookResult.None)
Result = showRetry(LibraryBook);
Status = Result switch
{
ProcessBookResult.Success => ProcessBookStatus.Completed,
ProcessBookResult.Cancelled => ProcessBookStatus.Cancelled,
_ => ProcessBookStatus.Failed,
};
}
return Result;
}
public async Task Cancel()
{
try
{
if (CurrentProcessable is AudioDecodable audioDecodable)
{
//There's some threadding bug that causes this to hang if executed synchronously.
await Task.Run(audioDecodable.Cancel);
}
}
catch (Exception ex)
{
Logger.Error(ex, $"{CurrentProcessable.Name}: Error while cancelling");
}
}
public void AddDownloadPdf() => AddProcessable<DownloadPdf>();
public void AddDownloadDecryptBook() => AddProcessable<DownloadDecryptBook>();
public void AddConvertToMp3() => AddProcessable<ConvertToMp3>();
private void AddProcessable<T>() where T : Processable, new()
{
Processes.Enqueue(() => new T());
}
public override string ToString() => LibraryBook.ToString();
#region Subscribers and Unsubscribers
private void LinkProcessable(Processable processable)
{
processable.Begin += Processable_Begin;
processable.Completed += Processable_Completed;
processable.StreamingProgressChanged += Streamable_StreamingProgressChanged;
processable.StreamingTimeRemaining += Streamable_StreamingTimeRemaining;
if (processable is AudioDecodable audioDecodable)
{
audioDecodable.RequestCoverArt += AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered += AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered += AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered += AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered += AudioDecodable_CoverImageDiscovered;
}
}
private void UnlinkProcessable(Processable processable)
{
processable.Begin -= Processable_Begin;
processable.Completed -= Processable_Completed;
processable.StreamingProgressChanged -= Streamable_StreamingProgressChanged;
processable.StreamingTimeRemaining -= Streamable_StreamingTimeRemaining;
if (processable is AudioDecodable audioDecodable)
{
audioDecodable.RequestCoverArt -= AudioDecodable_RequestCoverArt;
audioDecodable.TitleDiscovered -= AudioDecodable_TitleDiscovered;
audioDecodable.AuthorsDiscovered -= AudioDecodable_AuthorsDiscovered;
audioDecodable.NarratorsDiscovered -= AudioDecodable_NarratorsDiscovered;
audioDecodable.CoverImageDiscovered -= AudioDecodable_CoverImageDiscovered;
}
}
#endregion
#region AudioDecodable event handlers
private string title;
private string authorNames;
private string narratorNames;
private void AudioDecodable_TitleDiscovered(object sender, string title)
{
this.title = title;
updateBookInfo();
}
private void AudioDecodable_AuthorsDiscovered(object sender, string authors)
{
authorNames = authors;
updateBookInfo();
}
private void AudioDecodable_NarratorsDiscovered(object sender, string narrators)
{
narratorNames = narrators;
updateBookInfo();
}
private void updateBookInfo()
{
BookText = $"{title}\r\nBy {authorNames}\r\nNarrated by {narratorNames}";
}
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{
byte[] coverData = PictureStorage
.GetPictureSynchronously(
new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500));
AudioDecodable_CoverImageDiscovered(this, coverData);
return coverData;
}
private void AudioDecodable_CoverImageDiscovered(object sender, byte[] coverArt)
{
Cover = Dinah.Core.Drawing.ImageReader.ToImage(coverArt);
}
#endregion
#region Streamable event handlers
private void Streamable_StreamingTimeRemaining(object sender, TimeSpan timeRemaining)
{
TimeRemaining = timeRemaining;
}
private void Streamable_StreamingProgressChanged(object sender, Dinah.Core.Net.Http.DownloadProgress downloadProgress)
{
if (!downloadProgress.ProgressPercentage.HasValue)
return;
if (downloadProgress.ProgressPercentage == 0)
TimeRemaining = TimeSpan.Zero;
else
Progress = (int)downloadProgress.ProgressPercentage;
}
#endregion
#region Processable event handlers
private void Processable_Begin(object sender, LibraryBook libraryBook)
{
Status = ProcessBookStatus.Working;
Logger.Info($"{Environment.NewLine}{((Processable)sender).Name} Step, Begin: {libraryBook.Book}");
title = libraryBook.Book.Title;
authorNames = libraryBook.Book.AuthorNames();
narratorNames = libraryBook.Book.NarratorNames();
updateBookInfo();
}
private async void Processable_Completed(object sender, LibraryBook libraryBook)
{
Logger.Info($"{((Processable)sender).Name} Step, Completed: {libraryBook.Book}");
UnlinkProcessable((Processable)sender);
if (Processes.Count > 0)
{
NextProcessable();
LinkProcessable(CurrentProcessable);
var result = await CurrentProcessable.ProcessSingleAsync(libraryBook, validate: true);
if (result.HasErrors)
{
foreach (var errorMessage in result.Errors.Where(e => e != "Validation failed"))
Logger.Error(errorMessage);
Completed?.Invoke(this, EventArgs.Empty);
}
}
else
{
Completed?.Invoke(this, EventArgs.Empty);
}
}
#endregion
#region Failure Handler
private ProcessBookResult showRetry(LibraryBook libraryBook)
{
Logger.Error("ERROR. All books have not been processed. Most recent book: processing failed");
DialogResult? dialogResult = Configuration.Instance.BadBook switch
{
Configuration.BadBookAction.Abort => DialogResult.Abort,
Configuration.BadBookAction.Retry => DialogResult.Retry,
Configuration.BadBookAction.Ignore => DialogResult.Ignore,
Configuration.BadBookAction.Ask => null,
_ => null
};
string details;
try
{
static string trunc(string str)
=> string.IsNullOrWhiteSpace(str) ? "[empty]"
: (str.Length > 50) ? $"{str.Truncate(47)}..."
: str;
details =
$@" Title: {libraryBook.Book.Title}
ID: {libraryBook.Book.AudibleProductId}
Author: {trunc(libraryBook.Book.AuthorNames())}
Narr: {trunc(libraryBook.Book.NarratorNames())}";
}
catch
{
details = "[Error retrieving details]";
}
// if null then ask user
dialogResult ??= MessageBox.Show(string.Format(SkipDialogText + "\r\n\r\nSee Settings to avoid this box in the future.", details), "Skip importing this book?", SkipDialogButtons, MessageBoxIcon.Question, SkipDialogDefaultButton);
if (dialogResult == DialogResult.Abort)
return ProcessBookResult.FailedAbort;
if (dialogResult == SkipResult)
{
libraryBook.Book.UpdateBookStatus(LiberatedStatus.Error);
Logger.Info($"Error. Skip: [{libraryBook.Book.AudibleProductId}] {libraryBook.Book.Title}");
return ProcessBookResult.FailedSkip;
}
return ProcessBookResult.FailedRetry;
}
private string SkipDialogText => @"
An error occurred while trying to process this book.
{0}
- ABORT: Stop processing books.
- RETRY: retry this book later. Just skip it for now. Continue processing books. (Will try this book again later.)
- IGNORE: Permanently ignore this book. Continue processing books. (Will not try this book again later.)
".Trim();
private MessageBoxButtons SkipDialogButtons => MessageBoxButtons.AbortRetryIgnore;
private MessageBoxDefaultButton SkipDialogDefaultButton => MessageBoxDefaultButton.Button1;
private DialogResult SkipResult => DialogResult.Ignore;
}
#endregion
}

View File

@@ -0,0 +1,227 @@
namespace LibationWinForms.ProcessQueue
{
partial class ProcessBookControl
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(ProcessBookControl));
this.pictureBox1 = new System.Windows.Forms.PictureBox();
this.progressBar1 = new System.Windows.Forms.ProgressBar();
this.remainingTimeLbl = new System.Windows.Forms.Label();
this.etaLbl = new System.Windows.Forms.Label();
this.cancelBtn = new System.Windows.Forms.Button();
this.statusLbl = new System.Windows.Forms.Label();
this.bookInfoLbl = new System.Windows.Forms.Label();
this.moveUpBtn = new System.Windows.Forms.Button();
this.moveDownBtn = new System.Windows.Forms.Button();
this.moveFirstBtn = new System.Windows.Forms.Button();
this.moveLastBtn = new System.Windows.Forms.Button();
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
this.SuspendLayout();
//
// pictureBox1
//
this.pictureBox1.Location = new System.Drawing.Point(2, 2);
this.pictureBox1.Name = "pictureBox1";
this.pictureBox1.Size = new System.Drawing.Size(80, 80);
this.pictureBox1.SizeMode = System.Windows.Forms.PictureBoxSizeMode.StretchImage;
this.pictureBox1.TabIndex = 0;
this.pictureBox1.TabStop = false;
//
// progressBar1
//
this.progressBar1.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.progressBar1.Location = new System.Drawing.Point(88, 65);
this.progressBar1.MarqueeAnimationSpeed = 0;
this.progressBar1.Name = "progressBar1";
this.progressBar1.Size = new System.Drawing.Size(212, 17);
this.progressBar1.TabIndex = 2;
//
// remainingTimeLbl
//
this.remainingTimeLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.remainingTimeLbl.AutoSize = true;
this.remainingTimeLbl.Location = new System.Drawing.Point(338, 65);
this.remainingTimeLbl.Name = "remainingTimeLbl";
this.remainingTimeLbl.Size = new System.Drawing.Size(30, 15);
this.remainingTimeLbl.TabIndex = 3;
this.remainingTimeLbl.Text = "--:--";
this.remainingTimeLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// etaLbl
//
this.etaLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.etaLbl.AutoSize = true;
this.etaLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.etaLbl.Location = new System.Drawing.Point(304, 66);
this.etaLbl.Name = "etaLbl";
this.etaLbl.Size = new System.Drawing.Size(28, 13);
this.etaLbl.TabIndex = 3;
this.etaLbl.Text = "ETA:";
this.etaLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// cancelBtn
//
this.cancelBtn.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right)));
this.cancelBtn.BackColor = System.Drawing.Color.Transparent;
this.cancelBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("cancelBtn.BackgroundImage")));
this.cancelBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.cancelBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.cancelBtn.ForeColor = System.Drawing.SystemColors.Control;
this.cancelBtn.Location = new System.Drawing.Point(348, 6);
this.cancelBtn.Margin = new System.Windows.Forms.Padding(0);
this.cancelBtn.Name = "cancelBtn";
this.cancelBtn.Size = new System.Drawing.Size(20, 20);
this.cancelBtn.TabIndex = 4;
this.cancelBtn.UseVisualStyleBackColor = false;
//
// statusLbl
//
this.statusLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
this.statusLbl.AutoSize = true;
this.statusLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.statusLbl.Location = new System.Drawing.Point(89, 66);
this.statusLbl.Name = "statusLbl";
this.statusLbl.Size = new System.Drawing.Size(50, 13);
this.statusLbl.TabIndex = 3;
this.statusLbl.Text = "[STATUS]";
this.statusLbl.TextAlign = System.Drawing.ContentAlignment.TopRight;
//
// bookInfoLbl
//
this.bookInfoLbl.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.bookInfoLbl.Font = new System.Drawing.Font("Segoe UI", 8F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.bookInfoLbl.Location = new System.Drawing.Point(89, 6);
this.bookInfoLbl.Name = "bookInfoLbl";
this.bookInfoLbl.Size = new System.Drawing.Size(219, 56);
this.bookInfoLbl.TabIndex = 1;
this.bookInfoLbl.Text = "[multi-\r\nline\r\nbook\r\n info]";
//
// moveUpBtn
//
this.moveUpBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Right)));
this.moveUpBtn.BackColor = System.Drawing.Color.Transparent;
this.moveUpBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveUpBtn.BackgroundImage")));
this.moveUpBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveUpBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveUpBtn.ForeColor = System.Drawing.SystemColors.Control;
this.moveUpBtn.Location = new System.Drawing.Point(314, 24);
this.moveUpBtn.Name = "moveUpBtn";
this.moveUpBtn.Size = new System.Drawing.Size(30, 17);
this.moveUpBtn.TabIndex = 5;
this.moveUpBtn.UseVisualStyleBackColor = false;
//
// moveDownBtn
//
this.moveDownBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Right)));
this.moveDownBtn.BackColor = System.Drawing.Color.Transparent;
this.moveDownBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveDownBtn.BackgroundImage")));
this.moveDownBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveDownBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveDownBtn.ForeColor = System.Drawing.SystemColors.Control;
this.moveDownBtn.Location = new System.Drawing.Point(314, 40);
this.moveDownBtn.Name = "moveDownBtn";
this.moveDownBtn.Size = new System.Drawing.Size(30, 17);
this.moveDownBtn.TabIndex = 5;
this.moveDownBtn.UseVisualStyleBackColor = false;
//
// moveFirstBtn
//
this.moveFirstBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Right)));
this.moveFirstBtn.BackColor = System.Drawing.Color.Transparent;
this.moveFirstBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveFirstBtn.BackgroundImage")));
this.moveFirstBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveFirstBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveFirstBtn.ForeColor = System.Drawing.SystemColors.Control;
this.moveFirstBtn.Location = new System.Drawing.Point(314, 3);
this.moveFirstBtn.Name = "moveFirstBtn";
this.moveFirstBtn.Size = new System.Drawing.Size(30, 17);
this.moveFirstBtn.TabIndex = 5;
this.moveFirstBtn.UseVisualStyleBackColor = false;
//
// moveLastBtn
//
this.moveLastBtn.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Right)));
this.moveLastBtn.BackColor = System.Drawing.Color.Transparent;
this.moveLastBtn.BackgroundImage = ((System.Drawing.Image)(resources.GetObject("moveLastBtn.BackgroundImage")));
this.moveLastBtn.BackgroundImageLayout = System.Windows.Forms.ImageLayout.Zoom;
this.moveLastBtn.FlatStyle = System.Windows.Forms.FlatStyle.Flat;
this.moveLastBtn.ForeColor = System.Drawing.SystemColors.Control;
this.moveLastBtn.Location = new System.Drawing.Point(314, 63);
this.moveLastBtn.Name = "moveLastBtn";
this.moveLastBtn.Size = new System.Drawing.Size(30, 17);
this.moveLastBtn.TabIndex = 5;
this.moveLastBtn.UseVisualStyleBackColor = false;
//
// ProcessBookControl
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.SystemColors.ControlLight;
this.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle;
this.Controls.Add(this.moveLastBtn);
this.Controls.Add(this.moveDownBtn);
this.Controls.Add(this.moveFirstBtn);
this.Controls.Add(this.moveUpBtn);
this.Controls.Add(this.cancelBtn);
this.Controls.Add(this.statusLbl);
this.Controls.Add(this.etaLbl);
this.Controls.Add(this.remainingTimeLbl);
this.Controls.Add(this.progressBar1);
this.Controls.Add(this.bookInfoLbl);
this.Controls.Add(this.pictureBox1);
this.Margin = new System.Windows.Forms.Padding(4, 2, 4, 2);
this.Name = "ProcessBookControl";
this.Size = new System.Drawing.Size(375, 86);
((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.PictureBox pictureBox1;
private System.Windows.Forms.ProgressBar progressBar1;
private System.Windows.Forms.Label remainingTimeLbl;
private System.Windows.Forms.Label etaLbl;
private System.Windows.Forms.Label statusLbl;
private System.Windows.Forms.Label bookInfoLbl;
public System.Windows.Forms.Button cancelBtn;
public System.Windows.Forms.Button moveUpBtn;
public System.Windows.Forms.Button moveDownBtn;
public System.Windows.Forms.Button moveFirstBtn;
public System.Windows.Forms.Button moveLastBtn;
}
}

View File

@@ -0,0 +1,175 @@
using System;
using System.Drawing;
using System.Windows.Forms;
namespace LibationWinForms.ProcessQueue
{
internal partial class ProcessBookControl : UserControl
{
private static int ControlNumberCounter = 0;
/// <summary>
/// The contol's position within <see cref="VirtualFlowControl"/>
/// </summary>
public int ControlNumber { get; }
private ProcessBookStatus Status { get; set; } = ProcessBookStatus.Queued;
private readonly int CancelBtnDistanceFromEdge;
private readonly int ProgressBarDistanceFromEdge;
public static Color FailedColor = Color.LightCoral;
public static Color CancelledColor = Color.Khaki;
public static Color QueuedColor = SystemColors.Control;
public static Color SuccessColor = Color.PaleGreen;
public ProcessBookControl()
{
InitializeComponent();
statusLbl.Text = "Queued";
remainingTimeLbl.Visible = false;
progressBar1.Visible = false;
etaLbl.Visible = false;
CancelBtnDistanceFromEdge = Width - cancelBtn.Location.X;
ProgressBarDistanceFromEdge = Width - progressBar1.Location.X - progressBar1.Width;
ControlNumber = ControlNumberCounter++;
}
public void SetCover(Image cover)
{
pictureBox1.Image = cover;
}
public void SetBookInfo(string title)
{
bookInfoLbl.Text = title;
}
public void SetProgrss(int progress)
{
//Disable slow fill
//https://stackoverflow.com/a/5332770/3335599
if (progress < progressBar1.Maximum)
progressBar1.Value = progress + 1;
progressBar1.Value = progress;
}
public void SetRemainingTime(TimeSpan remaining)
{
remainingTimeLbl.Text = $"{remaining:mm\\:ss}";
}
public void SetResult(ProcessBookResult result)
{
string statusText = default;
switch (result)
{
case ProcessBookResult.Success:
statusText = "Finished";
Status = ProcessBookStatus.Completed;
break;
case ProcessBookResult.Cancelled:
statusText = "Cancelled";
Status = ProcessBookStatus.Cancelled;
break;
case ProcessBookResult.FailedRetry:
statusText = "Error, will retry later";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.FailedSkip:
statusText = "Error, Skippping";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.FailedAbort:
statusText = "Error, Abort";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.ValidationFail:
statusText = "Validion fail";
Status = ProcessBookStatus.Failed;
break;
case ProcessBookResult.None:
statusText = "UNKNOWN";
Status = ProcessBookStatus.Failed;
break;
}
SetStatus(Status, statusText);
}
public void SetStatus(ProcessBookStatus status, string statusText = null)
{
Color backColor = default;
switch (status)
{
case ProcessBookStatus.Completed:
backColor = SuccessColor;
Status = ProcessBookStatus.Completed;
break;
case ProcessBookStatus.Cancelled:
backColor = CancelledColor;
Status = ProcessBookStatus.Cancelled;
break;
case ProcessBookStatus.Queued:
backColor = QueuedColor;
Status = ProcessBookStatus.Queued;
break;
case ProcessBookStatus.Working:
backColor = QueuedColor;
Status = ProcessBookStatus.Working;
break;
case ProcessBookStatus.Failed:
backColor = FailedColor;
Status = ProcessBookStatus.Failed;
break;
}
SuspendLayout();
cancelBtn.Visible = Status is ProcessBookStatus.Queued or ProcessBookStatus.Working;
moveLastBtn.Visible = Status == ProcessBookStatus.Queued;
moveDownBtn.Visible = Status == ProcessBookStatus.Queued;
moveUpBtn.Visible = Status == ProcessBookStatus.Queued;
moveFirstBtn.Visible = Status == ProcessBookStatus.Queued;
remainingTimeLbl.Visible = Status == ProcessBookStatus.Working;
progressBar1.Visible = Status == ProcessBookStatus.Working;
etaLbl.Visible = Status == ProcessBookStatus.Working;
statusLbl.Visible = Status != ProcessBookStatus.Working;
statusLbl.Text = statusText ?? Status.ToString();
BackColor = backColor;
int deltaX = Width - cancelBtn.Location.X - CancelBtnDistanceFromEdge;
if (Status is ProcessBookStatus.Queued or ProcessBookStatus.Working && deltaX != 0)
{
//If the last book to occupy this control before resizing was not
//queued, the buttons were not Visible so the Anchor property was
//ignored. Manually resize and reposition everyhting
cancelBtn.Location = new Point(cancelBtn.Location.X + deltaX, cancelBtn.Location.Y);
moveFirstBtn.Location = new Point(moveFirstBtn.Location.X + deltaX, moveFirstBtn.Location.Y);
moveUpBtn.Location = new Point(moveUpBtn.Location.X + deltaX, moveUpBtn.Location.Y);
moveDownBtn.Location = new Point(moveDownBtn.Location.X + deltaX, moveDownBtn.Location.Y);
moveLastBtn.Location = new Point(moveLastBtn.Location.X + deltaX, moveLastBtn.Location.Y);
etaLbl.Location = new Point(etaLbl.Location.X + deltaX, etaLbl.Location.Y);
remainingTimeLbl.Location = new Point(remainingTimeLbl.Location.X + deltaX, remainingTimeLbl.Location.Y);
progressBar1.Width = Width - ProgressBarDistanceFromEdge - progressBar1.Location.X;
}
if (status == ProcessBookStatus.Working)
{
bookInfoLbl.Width = cancelBtn.Location.X - bookInfoLbl.Location.X - bookInfoLbl.Padding.Left + cancelBtn.Padding.Right;
}
else
{
bookInfoLbl.Width = moveLastBtn.Location.X - bookInfoLbl.Location.X - bookInfoLbl.Padding.Left + moveLastBtn.Padding.Right;
}
ResumeLayout();
}
public override string ToString()
{
return bookInfoLbl.Text ?? "[NO TITLE]";
}
}
}

View File

@@ -0,0 +1,868 @@
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<metadata name="pictureBox1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="progressBar1.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="remainingTimeLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="etaLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="cancelBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="cancelBtn.BackgroundImage" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
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
/x9W31o+WFcHNAAAAABJRU5ErkJggg==
</value>
</data>
<metadata name="statusLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="bookInfoLbl.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<metadata name="moveUpBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="moveUpBtn.BackgroundImage" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAMgAAABNCAYAAADjJSv1AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAATRSURBVHhe7Zlfp6ZVHIaHISIi
Oo2I6ANEDDFERx3FMERERN8gIoaIoc8QERERnQ5DREQfICKio4iOYs/vmndmrNlz79nPs55nrWf9uS8u
xn0y27vf2/rd9pWzszNr7QXK0Fp7UobW2pMytNaelKG19qQMrbUnZWitPSlDa+1JGaI5jHfCZ07/NDWR
PVAhmuq8GP4Y8uH/Fr4amorIHqgQTVXeDv8O+eAf+l/4UWgqIXugQjRVeDb8MkyLcd7vQ14XUxjZAxWi
Kc5rIadUWoaL/CvklTEFkT1QIZqifBxyQqUlWOLt0AO+ELIHKkRTBE6lH8L0S79WD/hCyB6oEM3ucCJx
KqVf9lw94Asge6BCNLvBScRplH7B99IDfkdkD1SIZhc4hZYO8Vz/DN8KzUZkD1SIZjOcQDlDPNcvQg/4
DcgeqBBNNpw8nD7pl7eWv4Ye8JnIHqgQTRacOpw86Ze2trxaH4ZmJbIHKkSzipJDPFcP+JXIHqgQzWJq
DPFcPeBXIHugQjSLqD3Ec/WAX4DsgQrRPJUjh3iuHvCXIHugQjQX0sIQz9UD/inIHqgQzRNwonCqpF+4
Xv0ufCE0CbIHKkTzGJwmnCjpl6x3eQWvh+YBsgcqRPMITpIehniun4ce8IHsgQrR3D9BOEXSL9Oo/hJO
P+BlD1SIk8Pp0esQz3X6AS97oEKcFE4NTo70izOb0w542QMV4oRwYnBqpF+WWZ1ywMseqBAn44Pw3zD9
ktjJBrzsgQpxEmYa4rlOM+BlD1SIEzDjEM+V15VXdmhkD1SIA+Mhni+v7fPhkMgeqBAHxUN8u7y6b4bD
IXugQhwQD/F9vRUONeBlD1SIA8FJ4CFexp/DYQa87IEKcRA4BTzEyzrMgJc9UCF2Dk8/J0D6i7Rl7X7A
yx6oEDvmlZCnP/3l2Tp2PeBlD1SInfJ+6CF+vF0OeNkDFWJn8LR/G6a/JHusvOK85t0ge6BC7Aie9D/C
9Jdj25DXnFe9C2QPVIgdcDXkKf8/TH8ptj153Zsf8LIHKsTG8RDvT175pge87IEKsWE8xPuV177ZAS97
oEJsEA/xcWxywMseqBAbw0N8PJsb8LIHKsRG8BAf32YGvOyBCrEBXg49xOewiQEve6BCPJj3Qg/xuXw4
4LkaDkH2QIV4EDy134TpB2fnkquB66E6sgcqxAO4Fv4eph+WnVOuB66IqsgeqBArwpP6Weghbs/LNVFt
wMseqBArwVP6U5h+KNamclVUGfCyByrECtwMPcTtEqsMeNkDFWJBngu/DtMPwNolFh3wsgcqxEK8EXqI
2y0WG/CyByrEnfEQt3u7+4CXPVAh7oiHuC0l1wh/HtgF2QMV4k54iNvScpVwnWwe8LIHKsSNeIjb2nKl
bBrwsgcqxA14iNuj5FrhaslC9kCFmAFP3Kehh7g9Wq6X1QNe9kCFuBKetrth+kNae6SrB7zsgQpxBTfC
f8L0h7O2BVcNeNkDFeICGOJfhekPZG2LLhrwsgcqxEt4PfQQtz156YCXPVAhXgBP1Sehh7jtVQY8188T
yB6oEAUvhXfC9D+ztke5fvhzxGPIHqgQz/Fu6CFuR5IriD9LPBrwsgcqxAd4iNvR5c8T9we87IEKMfAQ
t7PIdXRT9kCF1lo8u3IPfFOKqVljg2IAAAAASUVORK5CYII=
</value>
</data>
<metadata name="moveDownBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="moveDownBtn.BackgroundImage" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAMgAAABNCAYAAADjJSv1AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAATWSURBVHhe7Zlfh+1lGIaHiIiI
6DQiog8QERHRUacRERHRN4hNRET0GSIiIvoAERERnUZEdBTRUUzP1dLe7559zTtr1vr9/90XF3Uf7D17
Zt3e5zYXl5eXMcZr9PDi4o3yr5L/iXHL/lI+bz1ADw88VX5Xtn9YjFvys/LRUnuAHt7jofJO+U/Z/sEx
rtk/y9fLu1gP0MMHeaHkKWr/khjX6Lcl19F9WA/QQ+ex8vOy/ctiXItcQe+XXEUPYD1AD/tkwMe1+d8Q
L6/FeoAe3gxP1Pdl+0XEuETvDvEe1gP08Dh4qj4oM+DjEuXKuW+I97AeoIe348Xy17L94mKcU3498cAQ
72E9QA9vDwP+i7L9ImOcWq4Zfi2hQ7yH9QA9PJ0M+DiXDHF+HXES1gP08Dwy4OPU8usHrpiTsR6gh+eT
AR+nkGuFq+VsrAfo4XBkwMex5Eq51RDvYT1AD4clAz4OKVcJ18mth3gP6wF6OA5vlhnw8Ry5RrhKBsd6
gB6Ox9NlBnw8xbOHeA/rAXo4Lg+XPJHtPz7G6xxsiPewHqCH08BT+VvZfjNibB10iPewHqCH08GT+WXZ
flNiHGWI97AeoIfTkwEf/3e0Id7DeoAezkMGfOTXAaMN8R7WA/RwPjLg9ynXA1fEbFgP0MP5yYDfj1wN
XA+zYj1AD5dBBvz25Vrgapgd6wF6uCzeKjPgtyXXweRDvIf1AD1cHs+UP5TtNzmuU66CWYZ4D+sBerhM
eIo/LNtvdlyPXAFcA4vEeoAeLpuXygz4dcnrzxWwWKwH6OHyebzMgF+HixniPawH6OF6yIBfrosb4j2s
B+jhusiAX56LHOI9rAfo4frIgF+Gix7iPawH6OF6yYCfz8UP8R7WA/Rw3WTATy+v9+KHeA/rAXq4Dd4u
/y7bH2QcVl5rXu3VYz1AD7cDT/6PZftDjcPIK81rvQmsB+jhtuDp/6hsf7jxdHmVeZ03hfUAPdwmL5cZ
8OfJa7zaId7DeoAebpcM+NPlFV71EO9hPUAPt08G/PHy6vL6bhrrAXq4DzLgb/ar8oly81gP0MP9kAHv
bnKI97AeoIf7IwP+npsd4j2sB+jhPuGU4KRoPyx7c9NDvIf1AD3cN++UexvwuxjiPawH6GHgxPipbD9E
W3U3Q7yH9QA9DMCp8XHZfpi2JK8kr2UorAfoYWh5pdzagOd13N0Q72E9QA/DVbY04HkVdznEe1gP0MNw
HWse8L+XvIZBsB6gh6HHGgf81+Xuh3gP6wF6GG5iLQOe1+7dMtyA9QA9DMfCycLp0n4olyKv3LNlOALr
AXoYbgOnCydM++Gc20/KR8pwJNYD9DCcAqfM3AM+Q/xErAfoYTgVTpq5BnyG+BlYD9DDcA6cNlMO+Azx
AbAeoIdhCKYY8BniA2E9QA/DUIw54DPEB8R6gB6GoRlywP9RZogPjPUAPQxjMMSA/6bMEB8B6wF6GMaC
k4jTqP3QHyOvz3tlGAnrAXoYxoYTiVOpLcF1/lw+V4YRsR6gh2EKOJU4mdoyXPXTMkN8AqwH6GGYEk6n
qwOe1+XVMkyE9QA9DFPDCcUpxTefV+XJMkyI9QA9DHPAKfXa4T/D1FgPUMMY40ENY4wHNYwxHtQwxnhQ
wxjjQQ1jjAc1jDEe1DDGiJcX/wKcO4zm90rrbQAAAABJRU5ErkJggg==
</value>
</data>
<metadata name="moveFirstBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="moveFirstBtn.BackgroundImage" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAMgAAABZCAYAAAB7Ymt4AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAAT8SURBVHhe7dlvp+ZVFMbxIWKI
iJ4OEdELiIiIIeZpRERERO8ghoiI6DUMERHRC4iIiOgFRERPI3oUp3WNWWPfa52z53fv+/dv//b34kNn
KR3n3Je9lnPr6urqWpZ7Rv8AjOTeSQ/KL0r6F4v/CBgFBQEqKAhQQUGACgoCVFAQoIKCABUUBKigIEAF
BQEqKAhQQUGACgoCVFAQoIKCABUUBKigIEAFBQEqKAhQQUGACgoCVFAQoIKCABWTC0KWz23zpSl/QdF3
5nlDVspJD8ovSmTxvGx+M2UZbvKXedOQFXLSg/KLElk0H5l/TVmCKb4wTxuyUFIP4sCRRaJV6XtTfujP
pVfnJUMWSOpBHDgye7QiaVUqP+yt9Pp8aMjMST2IA0dmi1YirUblB3wuHPAzJ/UgDhyZJVqFph7irf40
dw2ZIakHceDIxdEK1HKIt/rccMBfmNSDOHCkOVp5tPqUH961/Go44C9I6kEcONIUrTpaecoP7dr0an1g
SENSD+LAkbOy5CHeigO+IakHceDI5KxxiLfigD8zqQdx4MikrH2It+KAn5jUgzhwpJotD/FWHPATknoQ
B47cmD0c4q044J+Q1IM4cCRFK4pWlfID16tvzXOGhKQexIEjJ9FqohWl/JD1Tq/gG4YUST2IA0ceRytJ
D4d4q88MB/yjpB7EgSMPVxCtIuWH6ah+MRzwltSDOHCDR6tHr4d4Kw54S+pBHLhBo1VDK0f5wRnN0Ad8
6kEcuAGjFUOrRvlhGdWwB3zqQRy4wfK++ceUHxIMeMCnHsSBGyQjHeKthjrgUw/iwA2QEQ/xVnpd9coe
PqkHceAOHA7xdnptnzWHTepBHLiDhkP8cnp1XzeHTOpBHLgDhkN8Xp+awx3wqQdx4A4UrQQc4sv42Rzq
gE89iAN3kGgV4BBf1qEO+NSDOHCdR0+/VoDyF4llHeKATz2IA9dxXjR6+stfHtbR/QGfehAHrtO8ZzjE
t9ftAZ96EAeus+hp/8aUvyRsS6+4XvOuknoQB66j6En/w5S/HOyDXnO96t0k9SAOXAd5yugp/8+UvxTs
j173Lg741IM4cDsPh3h/9Mrv/oBPPYgDt+NwiPdLr/2uD/jUgzhwOwyH+HHs9oBPPYgDt7NwiB/PLg/4
1IM4cDsJh/jx7eqATz2IA7eDvGA4xMewmwM+9SAO3MZ513CIj8UPeG0NmyX1IA7cRtFT+7Upf3AYi7YG
bQ+bJPUgDtwGec38bsofFsak7UFbxOpJPYgDt2L0pH5iOMQRaZtY9YBPPYgDt1L0lP5kyh8KUNJWsdoB
n3oQB26FvGM4xDHFagd86kEcuAXzjPnKlD8AYIrFD/jUgzhwC+VVwyGOSyx6wKcexIGbORzimNsiB3zq
QRy4GcMhjqVoG9GfB2ZL6kEcuJnCIY6laSvRdjLLAZ96EAfuwnCIY23aUi4+4FMP4sBdEA5xbEXbiraW
5qQexIFriJ64+4ZDHFvT9tJ0wKcexIE7M3rafjTlNwlsqemATz2IA3dG3jZ/m/KbA/bg7AM+9SAO3ITo
EH9gym8I2KPJB3zqQRy4J+QVwyGOnkw64FMP4sDdED1VHxsOcfRKB7y2n2uTehAH7prcMT+Y8n8G9Ejb
j/4ckZJ6EAcu5C3DIY4j0RakP0ucHPCpB3HgHoVDHEenP088PuBTD+LAWTjEMQptRw8P+NMeXN36HzdL
jfkqyMbMAAAAAElFTkSuQmCC
</value>
</data>
<metadata name="moveLastBtn.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
<data name="moveLastBtn.BackgroundImage" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
iVBORw0KGgoAAAANSUhEUgAAAMgAAABZCAYAAAB7Ymt4AAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1
MAAA6mAAADqYAAAXb5JfxUYAAAAJcEhZcwAACwwAAAsMAT9AIsgAAAUASURBVHhe7Zntpq1lFIYXERER
+wAiIjqAiIiIfQYRERHRGURERMQ+hoiIiA4gIiKivxER/YroV6zGncbezxxjrrHmmvP9eJ73vS6uH+uW
vVdrz9szbuvq+vr6wP95w/zLVIC4ZX8xX4w9cHPwiGfM78z2D0Pckp+ZT5qpB24ODnnM/MD8x2z/YMSR
/dN83XxI7IGbg+O8ZOopav8SxBH91tR1dEDsgZuDm3nK/Nxs/zLEUdQV9L6pqygRe+Dm4HYY8Dia/w1x
80ZiD9wcnIaeqO/N9ptA7NGHQ7wi9sDNwenoqfrQZMBjj+rKORjiFbEHbg7uzsvmr2b7zSGuqX49kYZ4
ReyBm4Pz0ID/wmy/ScSl1TWjX0scHeIVsQduDi6DAY9rqSGuX0ecReyBm4PLYcDj0urXD7pizib2wM3B
NDDgcQl1rehquZjYAzcH08KAx7nUlXKnIV4Re+DmYHoY8Dilukp0ndx5iFfEHrg5mI83TQY8XqKuEV0l
kxN74OZgXp41GfB4jhcP8YrYAzcH8/O4qSey/Z9HvMnJhnhF7IGbg+XQU/mb2f4wEFsnHeIVsQduDpZF
T+aXZvtDQZxliFfEHrg5WAcGPLqzDfGK2AM3B+vBgEf9OmC2IV4Re+DmYF0Y8PtU14OuiNWIPXBz0AcM
+P2oq0HXw6rEHrg56AcG/PbVtaCrYXViD9wc9MdbJgN+W+o6WHyIV8QeuDnok+fMH8z2h4xjqqtglSFe
EXvg5qBf9BR/ZLY/bBxHXQG6Brok9sDNQf+8YjLgx1Kvv66Abok9cHMwBk+bDPgx7GaIV8QeuDkYCwZ8
v3Y3xCtiD9wcjAcDvj+7HOIVsQduDsaEAd+HXQ/xitgDNwdjw4Bfz+6HeEXsgZuD8WHAL69e7+6HeEXs
gZuD7fC2+bfZ/kPitOq11qs9PLEHbg62hZ78H832HxWnUa+0XutNEHvg5mB76On/2Gz/cfF89Srrdd4U
sQduDrbLqyYD/jL1Gg87xCtiD9wcbBsG/PnqFR56iFfEHrg52AcM+NPVq6vXd9PEHrg52A8M+Nv9yrxn
bp7YAzcH+4IBf9xNDvGK2AM3B/uEAf/IzQ7xitgDNwf7RaeETor2w7I3Nz3EK2IP3BzAO+beBvwuhnhF
7IGbAxA6MX4y2w/RVt3NEK+IPXBzAI5OjU/M9sO0JfVK6rUEI/bAzQFEXjO3NuD1Ou5uiFfEHrg5gGNs
acDrVdzlEK+IPXBzABUjD/jfTb2GcITYAzcHcBsjDvivzd0P8YrYAzcHcAqjDHi9du+acAuxB24O4C7o
ZNHp0n4oe1Gv3PMmnEDsgZsDuCs6XXTCtB/Otf3UfMKEE4k9cHMA56JTZu0BzxA/k9gDNwdwCTpp1hrw
DPELiD1wcwCXotNmyQHPEJ+A2AM3BzAVSwx4hvhExB64OYApmXPAM8QnJPbAzQHMwZQD/g+TIT4xsQdu
DmAuphjw35gM8RmIPXBzAHOik0inUfuhP0W9Pu+ZMBOxB+7hF7AUOpF0KrUluMmfzRdMmJG2B62HX8CS
6FTSydSWIfrAZIgvQNuD1sMvrq7u679F3Jn32x60Hn5BQXCfUhDEQgqCWEhBEAspCGIhBUEspCCIhRQE
sZCCIBZSEMRCCoJYSEEQCykIYiEFQSykIIiFFASxkIIgFlIQxEIKglhIQRALKQhiIQVBLKQgiIU3FOT6
6l8KOJAbKVKmPQAAAABJRU5ErkJggg==
</value>
</data>
<metadata name="$this.Locked" type="System.Boolean, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<value>True</value>
</metadata>
</root>

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