Compare commits

...
229 Commits
Author SHA1 Message Date
Robert McRackan efbefa2784 Increm ver 2025-03-01 10:44:49 -05:00
rmcrackan 51aabe5dd4 Merge pull request #1161 from Mbucari/master
Performance and UI tweaks
2025-02-28 22:48:49 -05:00
Michael Bucari-Tovo 1c668adff8 Deliminated category names in library exports with semicolon (#1107) 2025-02-28 17:35:58 -07:00
Michael Bucari-Tovo 4170dcc1d5 Chardonnay UI bug fixes and improvements
- Theme changes do not require restart
- Fix some text appearing black in dark mode
- Fix dialog boxes not appearing correctly on Windows
- Fix queue vertical scroll bar overlapping items
2025-02-28 17:34:55 -07:00
Michael Bucari-Tovo 68cfae1d58 Fix ever-widening Form1 when form size is restored. 2025-02-28 12:04:49 -07:00
Michael Bucari-Tovo a790c7535c Changes to default directories for file storage (#1112)
- Add My Music and Local Application Data to known directories
- Make %localappdata%\Libation the default settings folder on *nix machines
- Make %MyMusic%\Libation\Books the default books folder on *nix machines
2025-02-28 12:01:58 -07:00
Michael Bucari-Tovo 3b7d5a354f Re-add books to queue that failed or were cancelled. 2025-02-28 10:47:26 -07:00
MBucari a9375f1520 Improve file cache performance and add migration
LibraryCommands.GetCounts hits the file cache hard. The previous cache implementation was linear list, so finding an entry by ID was (n). When you consider that each book may have many files, the number of cache entries could grow to many multiples of the library size.

The new cache uses a dictionary with the ID as its key, and a CacheEntry list as its value.
2025-02-28 10:07:45 -07:00
Michael Bucari-Tovo 47c9fcb883 Improve LibrarySizeChanged performance 2025-02-27 22:56:30 -07:00
rmcrackan 5f5c9f65ed Merge pull request #1160 from Mbucari/master
Fixed stack overflow crash when movifying large databases
2025-02-27 21:41:33 -05:00
MBucari 1417a4b992 Improve library load performance 2025-02-27 19:16:36 -07:00
Michael Bucari-Tovo 2d6120f0c4 Get full library in LibrarySizeChanged event and pass as EventArgs
There are multiple subscribers to LibraryCommands.LibrarySizeChanged, and each one calls GetLibrary_Flat_NoTracking(). Passing the full library as an event argument speeds up all operations which happen after the library size changes.

Fix initial backup counts
2025-02-27 13:11:28 -07:00
Michael Bucari-Tovo 2a25b7e0ad Load MainWindow before library finishes loading like Classic 2025-02-27 11:17:18 -07:00
Michael Bucari-Tovo 4766ea7372 Improve NRE safety for quick filters 2025-02-27 10:10:26 -07:00
Michael Bucari-Tovo d195dd07dc Remove upload-release-assets package (out of support) 2025-02-27 09:30:08 -07:00
Michael Bucari-Tovo bcbb7610ad Refactor GetAllEntries methods for clarity and maintainability
d
2025-02-27 09:00:26 -07:00
Michael Bucari-Tovo 6c5773df24 Fix stack overflow exception when updating large databases (#1158) 2025-02-26 14:41:59 -07:00
rmcrackan 211f15af25 Update InstallOnMac.md 2025-02-25 10:03:11 -05:00
Robert McRackan e3b0f80016 incr ver: 12.0 2025-02-24 12:05:24 -05:00
Robert McRackan 7b2c7e49e5 fix unit tests 2025-02-23 11:11:25 -05:00
Robert McRackan 6a40d19393 Fix unit tests 2025-02-23 10:28:09 -05:00
Robert McRackan 3167744111 #1151 : default character replacement for colon is _ 2025-02-23 10:13:15 -05:00
Robert McRackan b6a3ba335a Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-02-22 22:54:12 -05:00
Robert McRackan fa1ddc726a #1151 - unicode colon is causing issues with Windows Chardonnay 2025-02-22 22:54:00 -05:00
rmcrackan b3b0662dec Update InstallOnMac.md
Apple can’t check app for malicious software
2025-02-19 17:09:26 -05:00
Robert McRackan 5cb22cfd24 Update AAXClean.Codecs 2025-02-17 20:07:50 -05:00
Michael Bucari-Tovo e911344850 Workaround for DataGridView filtering internal NullReferenceException 2025-02-10 10:20:18 -07:00
Michael Bucari-Tovo 8ec7e5a9d2 Revert "DataGridView filtering internal NullReferenceException **HACK**"
This reverts commit e1f749c3da.
2025-02-10 10:19:24 -07:00
Robert McRackan e1f749c3da DataGridView filtering internal NullReferenceException **HACK** 2025-02-10 11:23:27 -05:00
Robert McRackan ba060d15aa Merge branch 'master' of https://github.com/rmcrackan/Libation 2025-02-10 09:12:32 -05:00
Robert McRackan 93fde236c8 format for clarity 2025-02-10 09:12:29 -05:00
Michael Bucari-Tovo 13aad1a7cb Restrict audio sample rate settings to allowed values (#1116) 2025-01-16 10:35:58 -07:00
rmcrackan 65c64c4504 Update Docker.md 2025-01-13 09:28:55 -05:00
rmcrackan 14ba04c28b Update Docker.md 2025-01-13 09:26:52 -05:00
rmcrackan 96e886d207 Update FrequentlyAskedQuestions.md
Brazil login
2025-01-08 10:04:55 -05:00
Robert McRackan c7279574a9 Upgrade avalonia 2025-01-07 08:00:35 -05:00
rmcrackan a522e1ff7e Merge pull request #1072 from shuvashish76/patch-1
Update InstallOnLinux.md with repology badge
2024-12-08 08:56:32 -05:00
shuvashish76 9ebc4444bd Update InstallOnLinux.md 2024-12-07 06:24:34 +00:00
shuvashish76 525afdf050 Update InstallOnLinux.md 2024-12-07 06:03:46 +00:00
shuvashish76 983cdf6ad5 Update InstallOnLinux.md with repology badge 2024-12-07 05:38:35 +00:00
Robert McRackan 09bb32a435 incr ver 2024-12-06 13:10:37 -05:00
rmcrackan 817ef33fbd Merge pull request #1071 from pixil98/master
update docker image to dotnet 9
2024-12-06 13:07:39 -05:00
Aaron Reisman be52b496a6 Update docker image to .net 9 2024-12-06 10:29:47 -06:00
Robert McRackan c0c99db6fa Incr ver 2024-12-05 14:54:16 -05:00
rmcrackan a1c8fb5921 Merge pull request #1066 from Mbucari/master
Fix chardonnay window closing (#1065)
2024-12-05 14:53:11 -05:00
MBucari 4576c0e193 Updata Avalonia to 11.2.2 2024-12-04 19:12:59 -07:00
MBucari d592e9435e Fix main window closing when dialog is closed (#1065) 2024-12-04 19:11:36 -07:00
Robert McRackan 9ce6cb54ab incr ver 2024-11-22 17:18:39 -05:00
Robert McRackan c15d49fc64 publish profiles => .net 2024-11-22 17:04:08 -05:00
Robert McRackan 99be869aa9 yaml => .net9 2024-11-22 16:56:16 -05:00
Robert McRackan a0e875a79c Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-11-22 16:49:09 -05:00
Robert McRackan 6134becc70 Upgrade to .net9 2024-11-22 16:47:59 -05:00
rmcrackan eadf7cff79 Merge pull request #1050 from stickystyle/patch-1
set maxdepth to prevent traversal into subdirectories
2024-11-20 13:13:58 -05:00
Ryan Parrish 87ca76f9cb set maxdepth to prevent traversal into subdirectories 2024-11-20 11:32:02 -05:00
Robert McRackan 43d1019059 * Bug fix #1048: docker: Error when using SLEEP_TIME - "integer expression expected"
Thanks @charltonstanley !
2024-11-19 06:40:51 -05:00
Robert McRackan ed87ded77a Docker deployment fix 2024-11-14 22:51:02 -05:00
rmcrackan 56d4205360 Merge pull request #1042 from pixil98/master
Exclude docker build report artifact from release packing
2024-11-14 22:50:26 -05:00
Aaron Reisman 1f839606ae Add a pattern to exclude docker build artifact from release 2024-11-14 16:33:30 -06:00
Robert McRackan 6eebe652d4 Docker changes => pre-release 2024-11-14 15:22:28 -05:00
rmcrackan 5fff22a0e1 Merge pull request #1011 from pixil98/master
Run docker image as non-root user
2024-11-14 15:19:36 -05:00
Aaron Reisman cd7040cdc7 pretty up the workflows 2024-11-14 11:18:16 -06:00
Aaron Reisman 97b792868f Update documentation with new envvars 2024-11-14 10:54:35 -06:00
pixil98 984f931f67 Merge branch 'rmcrackan:master' into master 2024-11-14 01:04:16 -06:00
Aaron Reisman e0dd9b845a rework database handling 2024-11-14 00:59:28 -06:00
rmcrackan f1c8b320c2 Merge pull request #1039 from jwillikers/fix-metadata
Fix metainfo validation
2024-11-13 21:09:45 -05:00
Jordan Williams 9b7d0cd909 Fix metainfo validation 2024-11-13 15:14:14 -06:00
rmcrackan 99592ff84e Merge pull request #1036 from jwillikers/flatpak-files
Add some necessary files for the Flatpak
2024-11-13 15:36:18 -05:00
Jordan Williams f97cfe77f9 Add CI to validate the desktop file and appstream metainfo 2024-11-10 15:09:56 -06:00
Jordan Williams 2954cb961b Add metainfo and screenshots 2024-11-10 14:05:24 -06:00
Jordan Williams 1e29b98b82 Add categories and keywords to the desktop file 2024-11-10 14:05:24 -06:00
Robert McRackan 8b76da0dbe incr ver 2024-10-25 15:05:08 -04:00
Mbucari 0a749d2d88 Update Bundle_MacOS.sh 2024-10-25 10:51:56 -06:00
Aaron Reisman 9ed6c1fd0d cleanup 2024-10-22 10:07:37 -05:00
Aaron Reisman 9825e2b552 Build both platforms in one action 2024-10-22 09:27:00 -05:00
Aaron Reisman 011efe3676 remove unused configure step 2024-10-22 00:39:35 -05:00
Aaron Reisman 2bdcc221f5 Specify platform(?) 2024-10-22 00:28:27 -05:00
pixil98 21bedca367 Merge branch 'rmcrackan:master' into master 2024-10-21 23:55:31 -05:00
Aaron Reisman 074fe79ded Update docker workflow to try building on validate 2024-10-21 17:13:08 -05:00
Aaron Reisman ac8c090c4c Rework run script to support db mount better 2024-10-21 14:02:52 -05:00
Aaron Reisman ade693bebb Update docker readme 2024-10-19 01:54:37 -05:00
Aaron Reisman 9bc53e45cd large overhaul of docker run script 2024-10-19 01:31:03 -05:00
Aaron Reisman 7d4eaa11e7 Run docker image as non-root user 2024-10-18 00:13:19 -05:00
Robert McRackan 4521c5d5ed incr ver 2024-10-16 12:04:04 -04:00
rmcrackan eb39f994e1 Merge pull request #1006 from cbordeman/Friendly-name-filters-995
Fixed bug in main view quick filter binding.
2024-10-16 08:03:44 -04:00
Chris Bordeman c19833b34e Fixed bug in main view quick filter binding. 2024-10-15 21:46:22 -04:00
Robert McRackan 6dcf456d06 fix ver 2024-10-15 13:20:58 -04:00
rmcrackan 8a87462cf5 Merge pull request #997 from cbordeman/Friendly-name-filters-995
Implemented Name on Quick Filters.
2024-10-15 13:09:38 -04:00
Chris Bordeman 9da2a44eff Small fix 2024-10-15 00:13:40 -04:00
Chris Bordeman 7af8d8aa70 Remove some warnings. 2024-10-15 00:07:57 -04:00
Chris Bordeman 4801f37e7c Moved QuickFilters migration to AppScaffolding. 2024-10-14 23:59:05 -04:00
rmcrackan 4f5df44d40 Merge pull request #999 from cbordeman/Add-default-theme-setting
Implemented "System" option for theme.
2024-10-14 15:09:21 -04:00
Chris Bordeman 63e28b13c1 Implemented "System" option for theme. 2024-10-12 02:17:35 -04:00
Chris Bordeman f92b2b65b2 Implemented Name on Quick Filters. 2024-10-11 05:45:04 -04:00
rmcrackan f7a4a95e3b Merge pull request #994 from cbordeman/master
Disable shrink/expand on mouseover for all scrollbars.
2024-10-10 07:38:24 -04:00
Chris Bordeman 71b8e9e51c Disable shrink/expand on mouseover for all scrollbars. 2024-10-10 02:22:36 -04:00
Robert McRackan c6788ccb48 typo 2024-09-25 10:12:01 -04:00
Robert McRackan 0503ee1404 incr ver 2024-09-18 08:17:35 -04:00
rmcrackan 303192c6c3 Merge pull request #978 from muchtall/allow-UseCoverAsFolderIcon-on-linux
Allow Non-Windows installations to populate Windows folder artwork
2024-09-18 08:15:05 -04:00
muchtall 6e21e96aa2 Allow Non-Windows installations to populate Windows folder artwork (https://github.com/rmcrackan/Libation/issues/977) 2024-09-16 19:42:26 +00:00
Robert McRackan d1d0a7e487 db migration for IsFinished 2024-09-11 07:56:18 -04:00
Robert McRackan 2fd8ea91e1 New search field: Finished/IsFinished.
All work complete except db migration
2024-09-11 07:45:37 -04:00
Robert McRackan 92ee0b2e6d update dependencies 2024-09-11 07:20:54 -04:00
Robert McRackan f0b5ae1cdc New filter option: IsInSeries, InSeries 2024-09-09 14:22:05 -04:00
Robert McRackan eb659cc7d7 typo 2024-09-09 08:31:55 -04:00
Robert McRackan cdd5f229d3 incr ver 2024-09-05 09:12:19 -04:00
Robert McRackan 29edfb7c3f Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-09-05 09:07:54 -04:00
Robert McRackan c0cb454d45 Make recursive file enumerations safer 2024-09-05 09:07:47 -04:00
rmcrackan 970a77c9e9 Merge pull request #946 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.5
Bump SixLabors.ImageSharp from 3.1.4 to 3.1.5 in /Source/LibationUiBase
2024-07-22 14:05:40 -04:00
dependabot[bot] 3488c8e0f5 Bump SixLabors.ImageSharp from 3.1.4 to 3.1.5 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-22 17:45:08 +00:00
rmcrackan 5133720cc8 InstallOnMac.md updated instructions 2024-06-30 07:22:38 -04:00
rmcrackan 4150746f45 InstallOnMac.md typo 2024-06-29 08:08:57 -04:00
Robert McRackan 3a95e1e72f incr ver 2024-06-26 08:00:48 -04:00
rmcrackan c74e26e1af Update release.yml
Release titles: don't include "v" in front of version number
2024-06-26 07:54:11 -04:00
rmcrackan ae54c95d46 Update docker.yml
Rolling back docker build push action to attempt to fix build
2024-06-26 07:51:00 -04:00
Robert McRackan 56e6bd164b By user request, other Serilog.Sink packages are included for experimental use 2024-06-26 07:07:49 -04:00
rmcrackan 9b28bdceaa Merge pull request #923 from rmcrackan/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-17 16:33:34 -04:00
dependabot[bot] b7f7d9004d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 14:23:33 +00:00
rmcrackan 1be0991e62 Add Audibly to FAQ 2024-06-07 13:27:18 -04:00
rmcrackan ff8a2e59c5 Update FrequentlyAskedQuestions.md 2024-05-27 11:48:48 -04:00
Robert McRackan 453904261b Bug fix. Audible's api acts weird when page count gets to ~400 2024-05-16 07:53:19 -04:00
Robert McRackan f9340db90a update references. audible api bugfix 2024-05-15 06:55:25 -04:00
Robert McRackan 5cd329dd26 refactor 2024-05-13 15:23:37 -04:00
Robert McRackan b2a882b79d Bug fix #904 -- navigation bug with new Accessibility feature 2024-05-13 15:17:17 -04:00
Robert McRackan 75df78a2f7 Add OSVersion to logs 2024-05-13 14:01:34 -04:00
Robert McRackan 3ad52cbecc docker to .net8 2024-05-09 10:01:38 -04:00
Robert McRackan 27b2fe741c Add accessibility 2024-05-07 10:39:30 -04:00
Robert McRackan d19fe2250c Add accessibility text to grid stoplight buttons 2024-05-06 21:56:00 -04:00
Robert McRackan d16d0c8de2 Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-05-05 16:24:07 -04:00
Robert McRackan c213d5d9f6 Attempt to solve networking issue by disabling ipv6 2024-05-05 16:24:00 -04:00
rmcrackan c73a023572 Update FrequentlyAskedQuestions.md 2024-04-30 22:42:34 -04:00
rmcrackan 67389917fd Merge pull request #879 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.4
Bump SixLabors.ImageSharp from 3.1.3 to 3.1.4 in /Source/LibationUiBase
2024-04-15 17:03:13 -04:00
dependabot[bot] b3264d5f42 Bump SixLabors.ImageSharp from 3.1.3 to 3.1.4 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.3...v3.1.4)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-15 20:27:24 +00:00
Robert McRackan 962d9b550f "Unable to load module" should just be a warning, not error 2024-04-15 08:03:49 -04:00
Robert McRackan 91ce7272ae Bug fix #874 : Chardonnay wasn't allowing custom directories ending in "Books" 2024-04-04 08:13:37 -04:00
rmcrackan 2f64ca6856 Merge pull request #870 from ScubyG/patch-1
Update InstallOnLinux.md
2024-03-22 07:37:00 -04:00
Davy Jones cfe5db436c Update InstallOnLinux.md
Fixed typo
2024-03-22 01:23:52 +00:00
rmcrackan 3653fc8094 Merge pull request #863 from rmcrackan/dependabot/github_actions/softprops/action-gh-release-2
Bump softprops/action-gh-release from 1 to 2
2024-03-11 10:45:44 -04:00
dependabot[bot] 662c0ec871 Bump softprops/action-gh-release from 1 to 2
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-11 14:20:12 +00:00
rmcrackan 44d39eabdb Merge pull request #861 from rmcrackan/dependabot/nuget/Source/LibationUiBase/SixLabors.ImageSharp-3.1.3
Bump SixLabors.ImageSharp from 3.1.2 to 3.1.3 in /Source/LibationUiBase
2024-03-05 11:45:59 -05:00
dependabot[bot] a0550d5c97 Bump SixLabors.ImageSharp from 3.1.2 to 3.1.3 in /Source/LibationUiBase
Bumps [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) from 3.1.2 to 3.1.3.
- [Release notes](https://github.com/SixLabors/ImageSharp/releases)
- [Commits](https://github.com/SixLabors/ImageSharp/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: SixLabors.ImageSharp
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-05 16:38:00 +00:00
Robert McRackan 14eaca6d45 Update dependency. Security update 2024-03-05 11:36:36 -05:00
Robert McRackan 93ccc206ef Update dependencies 2024-03-04 14:02:42 -05:00
Robert McRackan d3f0fd711e Remove validation check: SeriesName is null . At least 1 exception found in the wild 2024-02-29 14:44:41 -05:00
rmcrackan 3f4604e877 Update FrequentlyAskedQuestions.md
reformat
2024-02-29 10:15:47 -05:00
rmcrackan c316709af8 Update FrequentlyAskedQuestions.md
Classic vs Chardonnay
2024-02-29 10:15:01 -05:00
Robert McRackan 221d5c7f1c paypal link 2024-02-19 10:32:26 -05:00
Robert McRackan 5a86a1a27b Typo 2024-02-13 21:42:24 -05:00
rmcrackan dc5e55de68 Merge pull request #848 from patienttruth/master
Update Docker.md
2024-02-13 07:22:33 -05:00
patienttruth ee37864a42 Update Docker.md
Included an explanation that you may have to manually input the InProgress variable.
2024-02-13 06:49:33 +00:00
Robert McRackan efe347667c Better error reporting in audible api -- incl. inner exceptions 2024-01-20 21:44:29 -05:00
Robert McRackan f27a18bdbb Better error reporting in audible api 2024-01-19 09:30:48 -05:00
Robert McRackan d1834659d9 incr ver 2024-01-12 09:08:14 -05:00
Robert McRackan 7842b521d7 Update dependencies. Notes about upgrading 2024-01-12 07:44:40 -05:00
Robert McRackan 0822f0229d Revert Avalonia. Bug in 11.0.6 2024-01-04 10:03:33 -05:00
Robert McRackan 26aee4d29d Merge branch 'master' of https://github.com/rmcrackan/Libation 2024-01-03 11:43:59 -05:00
Robert McRackan 17a80a23a8 AAXClean upgrade to .net8 2024-01-03 11:43:45 -05:00
rmcrackan e26fc9ca62 Merge pull request #808 from rmcrackan/dependabot/github_actions/actions/download-artifact-4
Bump actions/download-artifact from 3 to 4
2023-12-15 09:50:25 -05:00
rmcrackan a03ccf1143 Merge pull request #807 from rmcrackan/dependabot/github_actions/actions/upload-artifact-4
Bump actions/upload-artifact from 3 to 4
2023-12-15 09:50:11 -05:00
dependabot[bot] bb8dd615db Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 14:08:26 +00:00
dependabot[bot] 9022a2889f Bump actions/upload-artifact from 3 to 4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-15 14:08:20 +00:00
rmcrackan ef049a3b02 Merge pull request #805 from rmcrackan/dependabot/github_actions/actions/setup-dotnet-4
Bump actions/setup-dotnet from 3 to 4
2023-12-05 10:01:34 -05:00
dependabot[bot] 77409750aa Bump actions/setup-dotnet from 3 to 4
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 3 to 4.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-05 14:14:17 +00:00
Robert McRackan 1702130b01 Upgrade to .net8 2023-11-15 19:57:39 -05:00
Robert McRackan b6d1a7e3ba Upgrade to .net8 2023-11-15 19:53:26 -05:00
Robert McRackan 2907ba5c13 Add FAQ 2023-10-20 08:27:20 -04:00
Robert McRackan 6df6c79ac8 New locale: Brazil 2023-10-18 22:33:24 -04:00
rmcrackan 3a9ca5d827 Merge pull request #779 from rmcrackan/dependabot/github_actions/dwenegar/upload-release-assets-2
Bump dwenegar/upload-release-assets from 1 to 2
2023-10-13 14:41:26 -04:00
dependabot[bot] e1e663e327 Bump dwenegar/upload-release-assets from 1 to 2
Bumps [dwenegar/upload-release-assets](https://github.com/dwenegar/upload-release-assets) from 1 to 2.
- [Release notes](https://github.com/dwenegar/upload-release-assets/releases)
- [Commits](https://github.com/dwenegar/upload-release-assets/compare/v1...v2)

---
updated-dependencies:
- dependency-name: dwenegar/upload-release-assets
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-13 14:37:55 +00:00
Robert McRackan 4b00d5fd84 incr ver 2023-09-18 13:58:23 -04:00
rmcrackan 02dbf8aad0 Merge pull request #751 from Mbucari/master
Minor Bug Fixes
2023-09-18 13:56:53 -04:00
rmcrackan 8326389f5c Merge pull request #732 from rmcrackan/dependabot/github_actions/actions/checkout-4
Bump actions/checkout from 3 to 4
2023-09-18 13:56:33 -04:00
dependabot[bot] 34535b3ce1 Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 17:33:43 +00:00
rmcrackan 7e5366ab95 Merge pull request #738 from rmcrackan/dependabot/github_actions/docker/setup-buildx-action-3
Bump docker/setup-buildx-action from 2 to 3
2023-09-18 13:30:24 -04:00
dependabot[bot] 690de9bc5c Bump docker/setup-buildx-action from 2 to 3
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-18 17:25:20 +00:00
rmcrackan c976aa2bb2 Merge pull request #739 from rmcrackan/dependabot/github_actions/docker/login-action-3
Bump docker/login-action from 2 to 3
2023-09-18 13:25:07 -04:00
rmcrackan 27f659285d Merge pull request #740 from rmcrackan/dependabot/github_actions/docker/build-push-action-5
Bump docker/build-push-action from 4 to 5
2023-09-18 13:24:55 -04:00
rmcrackan 423a5e7720 Merge pull request #741 from rmcrackan/dependabot/github_actions/docker/setup-qemu-action-3
Bump docker/setup-qemu-action from 2 to 3
2023-09-18 13:24:44 -04:00
Michael Bucari-Tovo 9152e12fe1 Fix #748 2023-09-18 10:14:54 -06:00
Michael Bucari-Tovo f471c53139 Fix #734 2023-09-18 10:08:15 -06:00
dependabot[bot] 66d055bb90 Bump docker/setup-qemu-action from 2 to 3
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2 to 3.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](https://github.com/docker/setup-qemu-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:49 +00:00
dependabot[bot] 2bbb35363a Bump docker/build-push-action from 4 to 5
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 4 to 5.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:46 +00:00
dependabot[bot] 1d3687cf9e Bump docker/login-action from 2 to 3
Bumps [docker/login-action](https://github.com/docker/login-action) from 2 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v2...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-12 14:50:42 +00:00
Mbucari daf925157f Update AppScaffolding.csproj 2023-09-02 07:59:30 -06:00
Mbucari 40eec9e674 Merge pull request #730 from Mbucari/master
Fix #729
2023-09-01 18:46:42 -06:00
MBucari 6f0782053e Restore PDF functionality (#729) 2023-09-01 16:30:35 -06:00
Michael Bucari-Tovo 04ad033ba0 Remove appsettings.json from program files dir 2023-08-31 09:36:38 -06:00
Robert McRackan f12f8ba3ee incr ver 2023-08-30 15:00:20 -04:00
rmcrackan cc6feb21ff Merge pull request #728 from Mbucari/master
Minor Bug fixes
2023-08-30 14:46:16 -04:00
Michael Bucari-Tovo c4f2ec428d Allow Settings form resizing 2023-08-30 12:38:28 -06:00
Michael Bucari-Tovo 59689cb647 Update Dependencies 2023-08-30 11:59:40 -06:00
Michael Bucari-Tovo d7f3758ebc Add support for environment variables and unix shortcuts in appsettings.json path. 2023-08-30 11:47:50 -06:00
Mbucari 49775b019c Update InstallOnMac.md 2023-08-30 09:06:57 -06:00
Michael Bucari-Tovo e55e969349 Fix issue with save button being disabled. 2023-08-30 08:59:20 -06:00
Mbucari 42a93bfac1 Update AAXClean 2023-08-27 19:47:32 -06:00
Mbucari f86c77a546 Create directory before trying to create appsettings.json 2023-08-27 19:47:00 -06:00
MBucari 88c35e2a56 Add scrolling view to settings window panels (#720) 2023-08-23 20:25:30 -06:00
MBucari b405e8b6b2 Remove unised response groups 2023-08-20 11:40:24 -06:00
MBucari 92d283187d Add support for locating mp3 audiobooks 2023-08-20 11:38:43 -06:00
Robert McRackan 51b8cfe71f incr ver 2023-08-17 14:03:40 -04:00
rmcrackan c80da5357b Merge pull request #716 from Mbucari/master
Fix NRE when item has no category ladders (#715)
2023-08-17 14:02:43 -04:00
Michael Bucari-Tovo 736d7c4a5f Fix NRE when item has no category ladders (#715) 2023-08-17 11:55:28 -06:00
Robert McRackan f175b7592e incr major ver 2023-08-12 21:45:11 -04:00
rmcrackan 415e6e7bc6 Merge pull request #710 from Mbucari/master
Batch of Fixxed Issues
2023-08-12 21:20:37 -04:00
MBucari d6a413e8d9 Update Avalonia to 11.0.3 2023-08-12 17:30:08 -06:00
Mbucari 3049de6246 Merge branch 'rmcrackan:master' into master 2023-08-12 17:20:09 -06:00
MBucari fb9b4eb77e Update audio duration on library scan (#707) 2023-08-12 17:19:56 -06:00
MBucari e65b6c76a8 Add ability to preview templates on user's books (#700)
Add template editor menu items to main grid context menu
2023-08-12 17:12:50 -06:00
Mbucari 167a021eb1 Change glass icon shading 2023-08-11 10:55:31 -06:00
Mbucari ff3ac2d6fd Fix MessageBox crash when UI has no Screens (#708) 2023-08-11 09:04:05 -06:00
Robert McRackan f733079a49 remove Moq 2023-08-10 15:24:43 -04:00
Mbucari 893d68190d Lazy load cover to improve startup time 2023-08-10 10:18:28 -06:00
Mbucari 97f94d8782 Add custom column widths to chardonnay 2023-08-07 15:54:50 -06:00
Mbucari 4b2ce0c2d1 Brighter stoplight colors (#702) 2023-08-07 11:31:22 -06:00
Mbucari ee00417c6f Add white glow to libation_glass.svg (#701) 2023-08-07 11:31:03 -06:00
Robert McRackan 768afd8ecd incr ver 2023-08-04 11:57:15 -04:00
rmcrackan 32c3fa85ce Merge pull request #699 from Mbucari/master
Fix broken template editor (#698)
2023-08-04 11:55:30 -04:00
Mbucari 6986c8f018 Fix broken template editor (#698) 2023-08-04 09:26:51 -06:00
Robert McRackan f69c2b1cfc incr ver 2023-08-02 21:55:19 -04:00
rmcrackan b11675c36a Merge pull request #696 from Mbucari/master
Bug fixes
2023-08-02 21:52:57 -04:00
Mbucari 379c2ed62d Fix account nickname retrieval (#629) 2023-08-02 13:54:49 -06:00
Mbucari 7c8489b52f Fix walkthrough causing freeze (#695) 2023-08-02 13:15:58 -06:00
rmcrackan c61a863edd Merge pull request #694 from Mbucari/master
Fix DPI scaling bug (#692)
2023-08-01 15:03:21 -04:00
Mbucari 1d54f32ef3 Fix DPI scaling bug (#692) 2023-08-01 11:55:23 -06:00
rmcrackan fabe4afd94 Merge pull request #691 from Mbucari/master
Fix #686 and enable nullable in FileManager and LibationFileManager
2023-07-30 20:22:55 -04:00
MBucari 61efa3c0c1 Update dependencies 2023-07-30 14:00:12 -06:00
MBucari fe70daf0bc Update avalonia to v11.0.1 2023-07-30 13:54:44 -06:00
MBucari 34033e7947 Enable Nullable 2023-07-30 13:31:57 -06:00
MBucari e8c63e9a6e Fix UI control overlapping label (#686) 2023-07-30 13:15:43 -06:00
Robert McRackan 9315165f80 update dependencies 2023-07-21 21:21:48 -04:00
206 changed files with 3778 additions and 1621 deletions

No files matched your search

+18 -18
View File
@@ -8,42 +8,42 @@ on:
inputs: inputs:
version_override: version_override:
type: string type: string
description: 'Version number override' description: "Version number override"
required: false required: false
run_unit_tests: run_unit_tests:
type: boolean type: boolean
description: 'Skip running unit tests' description: "Skip running unit tests"
required: false required: false
default: true default: true
runs_on: runs_on:
type: string type: string
description: 'The GitHub hosted runner to use' description: "The GitHub hosted runner to use"
required: true required: true
OS: OS:
type: string type: string
description: > description: >
The operating system targeted by the build. The operating system targeted by the build.
There must be a corresponding Bundle_$OS.sh script file in ./Scripts There must be a corresponding Bundle_$OS.sh script file in ./Scripts
required: true required: true
architecture: architecture:
type: string type: string
description: 'CPU architecture targeted by the build.' description: "CPU architecture targeted by the build."
required: true required: true
env: env:
DOTNET_CONFIGURATION: 'Release' DOTNET_CONFIGURATION: "Release"
DOTNET_VERSION: '7.0.x' DOTNET_VERSION: "9.0.x"
RELEASE_NAME: 'chardonnay' RELEASE_NAME: "chardonnay"
jobs: jobs:
build: build:
name: '${{ inputs.OS }}-${{ inputs.architecture }}' name: "${{ inputs.OS }}-${{ inputs.architecture }}"
runs-on: ${{ inputs.runs_on }} runs-on: ${{ inputs.runs_on }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-version: ${{ env.DOTNET_VERSION }}
env: env:
@@ -60,7 +60,7 @@ jobs:
version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')" version="$(grep -Eio -m 1 '<Version>.*</Version>' ./Source/AppScaffolding/AppScaffolding.csproj | sed -r 's/<\/?Version>//g')"
fi fi
echo "version=${version}" >> "${GITHUB_OUTPUT}" echo "version=${version}" >> "${GITHUB_OUTPUT}"
- name: Unit test - name: Unit test
if: ${{ inputs.run_unit_tests }} if: ${{ inputs.run_unit_tests }}
working-directory: ./Source working-directory: ./Source
@@ -69,7 +69,7 @@ jobs:
- name: Publish - name: Publish
id: publish id: publish
working-directory: ./Source working-directory: ./Source
run: | run: |
if [[ "${{ inputs.OS }}" == "MacOS" ]] if [[ "${{ inputs.OS }}" == "MacOS" ]]
then then
display_os="macOS" display_os="macOS"
@@ -78,13 +78,13 @@ jobs:
display_os="Linux" display_os="Linux"
RUNTIME_ID="linux-${{ inputs.architecture }}" RUNTIME_ID="linux-${{ inputs.architecture }}"
fi fi
OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}" OUTPUT="bin/Publish/${display_os}-${{ inputs.architecture }}-${{ env.RELEASE_NAME }}"
echo "display_os=${display_os}" >> $GITHUB_OUTPUT echo "display_os=${display_os}" >> $GITHUB_OUTPUT
echo "Runtime Identifier: $RUNTIME_ID" echo "Runtime Identifier: $RUNTIME_ID"
echo "Output Directory: $OUTPUT" echo "Output Directory: $OUTPUT"
dotnet publish \ dotnet publish \
LibationAvalonia/LibationAvalonia.csproj \ LibationAvalonia/LibationAvalonia.csproj \
--runtime $RUNTIME_ID \ --runtime $RUNTIME_ID \
@@ -122,9 +122,9 @@ jobs:
${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}" ${SCRIPT} "${BUNDLE_DIR}" "${{ steps.get_version.outputs.version }}" "${{ inputs.architecture }}"
artifact=$(ls ./bundle) artifact=$(ls ./bundle)
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}" echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
- name: Publish bundle - name: Publish bundle
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.bundle.outputs.artifact }} name: ${{ steps.bundle.outputs.artifact }}
path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }} path: ./Source/bin/Publish/bundle/${{ steps.bundle.outputs.artifact }}
+9 -9
View File
@@ -8,21 +8,21 @@ on:
inputs: inputs:
version_override: version_override:
type: string type: string
description: 'Version number override' description: "Version number override"
required: false required: false
run_unit_tests: run_unit_tests:
type: boolean type: boolean
description: 'Skip running unit tests' description: "Skip running unit tests"
required: false required: false
default: true default: true
env: env:
DOTNET_CONFIGURATION: 'Release' DOTNET_CONFIGURATION: "Release"
DOTNET_VERSION: '7.0.x' DOTNET_VERSION: "9.0.x"
jobs: jobs:
build: build:
name: '${{ matrix.os }}-${{ matrix.release_name }}' name: "${{ matrix.os }}-${{ matrix.release_name }}"
runs-on: windows-latest runs-on: windows-latest
strategy: strategy:
matrix: matrix:
@@ -35,9 +35,9 @@ jobs:
release_name: classic release_name: classic
prefix: Classic- prefix: Classic-
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} dotnet-version: ${{ env.DOTNET_VERSION }}
env: env:
@@ -107,9 +107,9 @@ jobs:
Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip" Compress-Archive -Path "${bin_dir}*" -DestinationPath "$artifact.zip"
- name: Publish artifact - name: Publish artifact
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: ${{ steps.zip.outputs.artifact }}.zip name: ${{ steps.zip.outputs.artifact }}.zip
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
if-no-files-found: error if-no-files-found: error
retention-days: 7 retention-days: 7
+6 -7
View File
@@ -8,22 +8,21 @@ on:
inputs: inputs:
version_override: version_override:
type: string type: string
description: 'Version number override' description: "Version number override"
required: false required: false
run_unit_tests: run_unit_tests:
type: boolean type: boolean
description: 'Skip running unit tests' description: "Skip running unit tests"
required: false required: false
default: true default: true
jobs: jobs:
windows: windows:
uses: ./.github/workflows/build-windows.yml uses: ./.github/workflows/build-windows.yml
with: with:
version_override: ${{ inputs.version_override }} version_override: ${{ inputs.version_override }}
run_unit_tests: ${{ inputs.run_unit_tests }} run_unit_tests: ${{ inputs.run_unit_tests }}
linux: linux:
strategy: strategy:
matrix: matrix:
@@ -36,7 +35,7 @@ jobs:
OS: ${{ matrix.OS }} OS: ${{ matrix.OS }}
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }} run_unit_tests: ${{ inputs.run_unit_tests }}
macos: macos:
strategy: strategy:
matrix: matrix:
@@ -47,4 +46,4 @@ jobs:
runs_on: macos-latest runs_on: macos-latest
OS: MacOS OS: MacOS
architecture: ${{ matrix.architecture }} architecture: ${{ matrix.architecture }}
run_unit_tests: ${{ inputs.run_unit_tests }} run_unit_tests: ${{ inputs.run_unit_tests }}
+31 -14
View File
@@ -8,7 +8,11 @@ on:
inputs: inputs:
version: version:
type: string type: string
description: 'Version number' description: "Version number"
required: true
release:
type: boolean
description: "Is this a release build?"
required: true required: true
secrets: secrets:
docker_username: docker_username:
@@ -16,31 +20,44 @@ on:
docker_token: docker_token:
required: true required: true
env:
DOCKER_IMAGE: ${{ secrets.docker_username }}/libation
jobs: jobs:
docker: build_and_push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v2 uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2 uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v2 if: ${{ inputs.release }}
uses: docker/login-action@v3
with: with:
username: ${{ secrets.docker_username }} username: ${{ secrets.docker_username }}
password: ${{ secrets.docker_token }} password: ${{ secrets.docker_token }}
- name: Build and push - name: Generate docker image tags
uses: docker/build-push-action@v4 id: metadata
uses: docker/metadata-action@v5
with: with:
push: true flavor: |
build-args: 'FOLDER_NAME=Linux-chardonnay' latest=true
tags: ${{ env.DOCKER_IMAGE }}:latest,${{ env.DOCKER_IMAGE }}:${{ inputs.version }} images: |
name=${{ secrets.docker_username }}/libation
tags: |
type=raw,value=${{ inputs.version }},enable=${{ inputs.release }}
- name: Build and push image
uses: docker/build-push-action@v6
with:
platforms: linux/amd64,linux/arm64
push: ${{ steps.metadata.outputs.tags != ''}}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
+12 -15
View File
@@ -5,7 +5,7 @@ name: release
on: on:
push: push:
tags: tags:
- 'v*' - "v*"
jobs: jobs:
prerelease: prerelease:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -15,7 +15,7 @@ jobs:
- name: Get tag version - name: Get tag version
id: get_version id: get_version
run: | run: |
export TAG='${{ github.ref_name }}' export TAG="${{ github.ref_name }}"
echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}" echo "version=${TAG#v}" >> "${GITHUB_OUTPUT}"
docker: docker:
@@ -23,6 +23,7 @@ jobs:
uses: ./.github/workflows/docker.yml uses: ./.github/workflows/docker.yml
with: with:
version: ${{ needs.prerelease.outputs.version }} version: ${{ needs.prerelease.outputs.version }}
release: true
secrets: secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }} docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }} docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -33,29 +34,25 @@ jobs:
with: with:
version_override: ${{ needs.prerelease.outputs.version }} version_override: ${{ needs.prerelease.outputs.version }}
run_unit_tests: false run_unit_tests: false
release: release:
needs: [prerelease,build] needs: [prerelease, build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Download artifacts - name: Download artifacts
uses: actions/download-artifact@v3 uses: actions/download-artifact@v4
with: with:
path: artifacts path: artifacts
pattern: "*(Classic-)Libation.*"
- name: Release - name: Release
id: release id: release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v2
with: with:
name: Libation v${{ needs.prerelease.outputs.version }} name: Libation ${{ needs.prerelease.outputs.version }}
body: <Put a body here> body: <Put a body here>
token: ${{ secrets.GITHUB_TOKEN }}
draft: true draft: true
prerelease: false prerelease: false
files: |
- name: Upload release assets artifacts/*/*
uses: dwenegar/upload-release-assets@v1
env:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
with:
release_id: '${{ steps.release.outputs.id }}'
assets_path: ./artifacts
@@ -0,0 +1,22 @@
name: Validate MetaInfo
"on":
pull_request:
branches: ["master"]
paths:
- .github/workflows/validate-appstream-metainfo.yml
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
push:
branches: ["master"]
paths:
- .github/workflows/validate-appstream-metainfo.yml
- Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
jobs:
validate-appstream-metainfo:
runs-on: ubuntu-latest
container:
image: ghcr.io/flathub/flatpak-builder-lint:latest
steps:
- uses: actions/checkout@v4
- name: Check the MetaInfo file
run: flatpak-builder-lint appstream Source/LoadByOS/LinuxConfigApp/com.getlibation.Libation.metainfo.xml
@@ -0,0 +1,21 @@
name: Check desktop file
"on":
pull_request:
branches: ["master"]
paths:
- .github/workflows/validate-desktop-file.yml
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
push:
branches: ["master"]
paths:
- .github/workflows/validate-desktop-file.yml
- Source/LoadByOS/LinuxConfigApp/Libation.desktop
jobs:
validate-desktop-file:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: sudo apt --yes install desktop-file-utils
- name: Check the desktop file
run: desktop-file-validate Source/LoadByOS/LinuxConfigApp/Libation.desktop
+9 -1
View File
@@ -1,5 +1,5 @@
# validate.yml # validate.yml
# Validates that Libation will build on a pull request or push to master. # Validates that Libation will build on a pull request or push to master.
--- ---
name: validate name: validate
@@ -12,3 +12,11 @@ on:
jobs: jobs:
build: build:
uses: ./.github/workflows/build.yml uses: ./.github/workflows/build.yml
docker:
uses: ./.github/workflows/docker.yml
with:
version: ${GITHUB_SHA}
release: false
secrets:
docker_username: ${{ secrets.DOCKERHUB_USERNAME }}
docker_token: ${{ secrets.DOCKERHUB_TOKEN }}
+3
View File
@@ -0,0 +1,3 @@
{
"LibationFiles": "/config-internal"
}
+156 -50
View File
@@ -1,68 +1,174 @@
#!/bin/bash #!/bin/bash
# Rewire echo to print date time error() {
echo() { log "ERROR" "$1"
if [[ -n $1 ]]; then
printf "$(date '+%F %T'): %s\n" "$1"
fi
} }
# ################################ warn() {
# Setup log "WARNING" "$1"
# ################################ }
echo "Starting"
if [[ -z "${SLEEP_TIME}" ]]; then
echo "No sleep time passed in. Will run once and exit."
else
echo "Sleep time is set to ${SLEEP_TIME}"
fi
echo "" info() {
log "info" "$1"
}
# Check if the config directory is passed in, and there is no link to it then create the link. debug() {
if [ -d "/config" ] && [ ! -d "/root/Libation" ]; then if [ "${LOG_LEVEL}" = "debug" ]; then
echo "Linking config directory to the Libation config directory" log "debug" "$1"
ln -s /config/ /root/Libation fi
fi }
# If no config error and exit log() {
if [ ! -d "/config" ]; then LEVEL=$1
echo "ERROR: No /config directory. You must pass in a volume containing your config mapped to /config" MESSAGE=$2
printf "$(date '+%F %T') %s: %s\n" "${LEVEL}" "${MESSAGE}"
}
init_config_file() {
FILE=$1
FULLPATH=${LIBATION_CONFIG_DIR}/${FILE}
if [ -f ${FULLPATH} ]; then
info "loading ${FILE}"
cp ${FULLPATH} ${LIBATION_CONFIG_INTERNAL}/
return 0
else
warn "${FULLPATH} not found, creating empty file"
echo "{}" > ${LIBATION_CONFIG_INTERNAL}/${FILE}
return 1
fi
}
update_settings() {
FILE=$1
KEY=$2
VALUE=$3
info "setting ${KEY} to ${VALUE}"
echo $(jq --arg k "${KEY}" --arg v "${VALUE}" '.[$k] = $v' ${LIBATION_CONFIG_INTERNAL}/${FILE}) > ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp
mv ${LIBATION_CONFIG_INTERNAL}/${FILE}.tmp ${LIBATION_CONFIG_INTERNAL}/${FILE}
}
is_mounted() {
DIR=$1
if grep -qs "${DIR} " /proc/mounts;
then
return 0
else
return 1
fi
}
create_db() {
DBFILE=$1
if [ -f "${DBFILE}" ]; then
warn "prexisting database found when creating"
return 0
else
if ! touch "${DBFILE}"; then
error "unable to create database, check permissions on host"
exit 1
fi
return 1
fi
}
setup_db() {
DBPATH=$1
dbpattern="*.db"
debug "using database directory ${DBPATH}"
# Figure out the right databse file
if [[ -z "${LIBATION_DB_FILE}" ]];
then
dbCount=$(find "${DBPATH}" -maxdepth 1 -type f -name "${dbpattern}" | wc -l)
if [ "${dbCount}" -gt 1 ];
then
error "too many database files found, set LIBATION_DB_FILE to the filename you wish to use"
exit 1
elif [ "${dbCount}" -eq 1 ];
then
files=( ${DBPATH}/${dbpattern} )
FILE=${files[0]}
else
FILE="${DBPATH}/LibationContext.db"
fi
else
FILE="${DBPATH}/${LIBATION_DB_FILE}"
fi
debug "planning to use database ${FILE}"
if [ -f "${FILE}" ]; then
info "database found at ${FILE}"
elif [ ${LIBATION_CREATE_DB} = "true" ];
then
warn "database not found, creating one at ${FILE}"
create_db ${FILE}
else
error "database not found and creation is disabled"
exit 1 exit 1
fi fi
ln -s "${FILE}" "${LIBATION_CONFIG_INTERNAL}/LibationContext.db"
}
# If user passes in db from a /db/ folder and a db does not already exist / is not already linked run() {
FILE=/db/LibationContext.db info "scanning accounts"
if [ -f "${FILE}" ] && [ ! -f "/config/LibationContext.db" ]; then /libation/LibationCli scan
echo "Linking passed in Libation database from /db/ to the Libation config directory" info "liberating books"
ln -s $FILE /config/LibationContext.db /libation/LibationCli liberate
fi }
# Confirm we have a db in the config direcotry. main() {
if [ ! -f "/config/LibationContext.db" ]; then info "initializing libation"
echo "ERROR: No Libation database detected, exiting." init_config_file AccountsSettings.json
exit 1 init_config_file Settings.json
fi
info "loading settings"
update_settings Settings.json Books /data
update_settings Settings.json InProgress /tmp
# ################################ info "loading database"
# Loop and liberate # If user provides a separate database mount, use that
# ################################ if is_mounted "${LIBATION_DB_DIR}";
while true then
do DB_LOCATION=${LIBATION_DB_DIR}
echo "" # Otherwise, use the config directory
echo "Scanning accounts" else
/libation/LibationCli scan DB_LOCATION=${LIBATION_CONFIG_DIR}
echo "Liberating books" fi
/libation/LibationCli liberate setup_db ${DB_LOCATION}
echo ""
# Try to warn if books dir wasn't mounted in
if ! is_mounted "${LIBATION_BOOKS_DIR}";
then
warn "${LIBATION_BOOKS_DIR} does not appear to be mounted, books will not be saved"
fi
# Let the user know what the run type will be
if [[ -z "${SLEEP_TIME}" ]]; then
SLEEP_TIME=-1
fi
if [ "${SLEEP_TIME}" == -1 ]; then
info "running once"
else
info "running every ${SLEEP_TIME}"
fi
# loop
while true
do
run
# Liberate only once if SLEEP_TIME was set to -1 # Liberate only once if SLEEP_TIME was set to -1
if [ "${SLEEP_TIME}" = -1 ]; then if [ "${SLEEP_TIME}" == -1 ]; then
break break
fi fi
echo "Sleeping for ${SLEEP_TIME}"
sleep "${SLEEP_TIME}" sleep "${SLEEP_TIME}"
done done
echo "Exiting" info "exiting"
}
main
+31 -14
View File
@@ -1,22 +1,39 @@
# Dockerfile # Dockerfile
FROM mcr.microsoft.com/dotnet/sdk:7.0 as build-env FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG TARGETARCH
COPY Source /Source COPY Source /Source
RUN dotnet publish -c Release -o /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj -p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml RUN dotnet publish \
COPY Docker/liberate.sh /Source/bin/Publish/Linux-chardonnay /Source/LibationCli/LibationCli.csproj \
--arch ${TARGETARCH} \
--configuration Release \
--output /Source/bin/Publish/Linux-chardonnay \
-p:PublishProfile=/Source/LibationCli/Properties/PublishProfiles/LinuxProfile.pubxml
FROM mcr.microsoft.com/dotnet/runtime:9.0
ARG USER_UID=1001
ARG USER_GID=1001
# Set the character set that will be used for folder and filenames when liberating
ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8
ENV SLEEP_TIME=-1
ENV LIBATION_CONFIG_INTERNAL=/config-internal
ENV LIBATION_CONFIG_DIR=/config
ENV LIBATION_DB_DIR=/db
ENV LIBATION_DB_FILE=
ENV LIBATION_CREATE_DB=true
ENV LIBATION_BOOKS_DIR=/data
FROM mcr.microsoft.com/dotnet/runtime:7.0 RUN apt-get update && apt-get -y upgrade && \
apt-get install -y jq && \
mkdir -m777 ${LIBATION_CONFIG_INTERNAL} ${LIBATION_BOOKS_DIR}
ENV SLEEP_TIME "30m" COPY --from=build /Source/bin/Publish/Linux-chardonnay /libation
COPY Docker/* /libation
# Sets the character set that will be used for folder and filenames when liberating USER ${USER_UID}:${USER_GID}
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8
RUN mkdir /db /config /data CMD ["/libation/liberate.sh"]
COPY --from=build-env /Source/bin/Publish/Linux-chardonnay /libation
CMD ["./libation/liberate.sh"]
+1 -1
View File
@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
+44 -12
View File
@@ -1,31 +1,32 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
> [!WARNING]
> ## Breaking Changes
> * The docker image now runs as user 1001 and group 1001. Make sure that the permissions on your volumes allow user 1001 to read and write to them or see the User section below for other options, or if you're not sure.
> * `SLEEP_TIME` is now set to `-1` by default. This means the image will run once and exit. If you were relying on the previous default, you'll need to explicitly set the `SLEEP_TIME` environment variable to `30m` to replicate the previous behavior.
> * The docker image now ignores the values in `Settings.json` for `Books` and `InProgress`. You can now change the folder that books are saved to by using the `LIBATION_BOOKS_DIR` environment variable.
### Disclaimer # Disclaimer
The docker image is provided as-is. We hope it can be useful to you but it is not officially supported. The docker image is provided as-is. We hope it can be useful to you but it is not officially supported.
### Setup ### Configuration
In order to use the docker image, you'll need to provide it with a copy of the `AccountsSettings.json`, `Settings.json`, and `LibationContext.db` files. These files can usually be found in the Libation folder in your user's home directory. If you haven't run Libation yet, you'll need to launch it to generate these files and setup your accounts. Once you have them, copy these files to a new location, such as `/opt/libation/config`. Before using them we'll need to make a couple edits so that the filepaths referenced are correct when running from the docker image. Configuration in Libation is handled by two files, `AccountsSettings.json` and `Settings.json`. These files can usually be found in the Libation folder in your user's home directory. The easiest way to configure these is to run the desktop version of Libation and then copy them into a folder, such as `/opt/libation/config`, that you'll volume mount into the image. `Settings.json` is technically optional, and, if not provided, Libation will run using the default settings. Additionally, the `Books` and `InProgress` settings in `Settings.json` will be ignored and the image will instead substitute it's own values.
In Settings.json, make the following changes:
* Change `Books` to `/data`
* Change `InProgress` to `/tmp`
### Running ### Running
Once the configuration files are copied and edited, the docker image can be run with the following command. Once the configuration files are copied, the docker image can be run with the following command.
``` ```
sudo docker run -d \ sudo docker run -d \
-v /opt/libation/config:/config \ -v /opt/libation/config:/config \
-v /opt/libation/books:/data \ -v /opt/libation/books:/data \
--name libation \ --name libation \
--restart=always \ --restart=always \
rmcrackan/libation rmcrackan/libation:latest
``` ```
By default the container will scan for new books every 30 minutes and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. Additionally, if you pass in `-1` it will scan and download books once and then exit. By default the container will scan for new books once and download any new ones. This is configurable by passing in a value for the `SLEEP_TIME` environment variable. For example, if you pass in `10m` it will keep running, scan for new books, and download them every 10 minutes.
``` ```
sudo docker run -d \ sudo docker run -d \
@@ -34,6 +35,37 @@ sudo docker run -d \
-e SLEEP_TIME='10m' \ -e SLEEP_TIME='10m' \
--name libation \ --name libation \
--restart=always \ --restart=always \
rmcrackan/libation rmcrackan/libation:latest
``` ```
### Environment Variables
| Env Var | Default | Description |
| -------- | ------- | ----------- |
| SLEEP_TIME | -1 | Length of time to sleep before doing another scan/download. Set to -1 to run one. |
| LIBATION_BOOKS_DIR | /data | Folder where books will be saved |
| LIBATION_CONFIG_DIR | /config | Folder to read configuration from. |
| LIBATION_DB_DIR | /db | Optional folder to load database from. If not mounted, will load database from `LIBATION_CONFIG_DIR`. |
| LIBATION_DB_FILE | | Name of database file to load. By default it will look for all `.db` files and load one if there is only one present. |
| LIBATION_CREATE_DB | true | Whether or not the image should create a database file if none are found. |
### User
This docker image runs as user `1001`. In order for the image to function properly, user `1001` must be able to read and write the volumes that are mounted in. If they are not, you will see errors, including [sqlite error](#1060), [Microsoft.Data.Sqlite.SqliteException](#1110), [unable to open database file](#1113), [Microsoft.EntityFrameworkCore.DbUpdateException](#1049)
If you're not sure what your user number is, check the output of the `id` command. Docker should normally run with the number of the user who configured and ran it.
If you want to change the user the image runs as, you can specify `-u <uid>:<gid>`. For example, to run it as user `2000` and group `3000`, you could do the following:
```
sudo docker run -d \
-u 2000:3000 \
-v /opt/libation/config:/config \
-v /opt/libation/books:/data \
--name libation \
--restart=always \
rmcrackan/libation:latest
```
If the user it's running as is correct, and it still cannot write, be sure to check whether the files and/or folders might be owned by the wrong user. You can use the `chown` command to change the owner of the file to the correct user and group number, for example: `chown -R 1001:1001 /mnt/audiobooks /mnt/libation-config`
### Advanced Database Options
The docker image supports an optional database mount location defined by `LIBATION_DB_DIR`. This allows the database to be mounted as read/write, while allowing the rest of the configuration files to be mounted as read only. This is specifically useful if running in Kubernetes where you can use Configmaps and Secrets to define the configuration. If the `LIBATION_DB_DIR` is mounted, it will be used, otherwise it will look for the database in `LIBATION_CONFIG_DIR`. If it does not find the database in the expected location, it will attempt to make an empty database there.
+42
View File
@@ -0,0 +1,42 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Frequently Asked Questions
## Q: What's the difference between 'Classic' and 'Chardonnay'?
**A:** First and most importantly: Classic and Chardonnay have the exact same features.
* **Classic** is Windows only. Its older 'grey boxes' look has a compact design which allows for more information on the screen. Notably, Classic was written using an older, more mature technology which has built-in support for screenreaders.
* **Chardonnay** is available for Windows, Mac, and Linux. Its modern design has a more open look and feel.
## Q: Now that I've downloaded my books, how can I listen to them?
**A:** You can use any app which plays m4b files (or mp3 files if you used that setting). Here are just a few ideas. Disclaimer: I have no affiliation with any of these companies:
* iOS: [BookPlayer](https://apps.apple.com/us/app/bookplayer/id1138219998)
* iOS: [Bound](https://apps.apple.com/us/app/bound-audiobook-player/id1041727137)
* Android: [Smart AudioBook Player](https://play.google.com/store/apps/details?id=ak.alizandro.smartaudiobookplayer&hl=en_US&gl=US)
* Android: [Listen](https://play.google.com/store/apps/details?id=ru.litres.android.audio&hl=en_US&gl=US)
* Desktop: [VLC](https://www.videolan.org/)
* Windows Desktop: [Audibly](https://github.com/rstewa/Audibly) -- a desktop player build specifically for audiobooks
Self-hosting online:
* [audiobookshelf](https://www.audiobookshelf.org). On [reddit](https://www.reddit.com/r/audiobookshelf/)
* [plex](https://www.plex.tv/). Listen with [Prologue](https://prologue.audio/) (iOS)
## Q: I'm having trouble loggin into my Brazil account.
For reasons known only to Jeff Bezos and God, amazon and audible brazil handle logins slightly differently. [See this ticket for more details.](https://github.com/rmcrackan/Libation/issues/1103)
## Q: How do I use Libation with a South Africa account?
**A:** Like many countries, amazon gives South Africa it's own amazon site. [Unlike many other regions](https://www.audible.com/ep/country-selector) there is not South Africa specific audible site. Use `US` for your region -- ie: audible.com.
(Not exactly a *frequently* asked question but it's come up more than once)
+1 -1
View File
@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
+38 -6
View File
@@ -1,27 +1,59 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
### Install and Run Libation on Ubuntu ## Packaging status
New Libation releases are automatically packed into .deb and .rpm package and are available from the Libation repository's releases page. [![Packaging status](https://repology.org/badge/vertical-allrepos/libation.svg)](https://repology.org/project/libation/versions)
New Libation releases are automatically packed into `.deb` and `.rpm` package and are available from the Libation repository's releases page.
Run this command in your terminal to dowbnload and install Libation, replacing the url with the latest Libation package url: Run this command in your terminal to download and install Libation, replacing the url with the latest Libation package url:
- Debian ### Debian
```Console ```Console
wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb && wget -O libation.deb https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.deb &&
sudo apt install ./libation.deb sudo apt install ./libation.deb
``` ```
- Redhat and CentOS ### Redhat and CentOS
```Console ```Console
wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm && wget -O libation.rpm https://github.com/rmcrackan/Libation/releases/download/vX.X.X/Libation.X.X.X-linux-chardonnay.rpm &&
sudo yum install ./libation.rpm sudo yum install ./libation.rpm
``` ```
---
### Arch Linux
```Console
yay -S libation
```
This package is available on [Arch User Repository](https://aur.archlinux.org/packages/libation), install via your choice of [AUR helpers](https://wiki.archlinux.org/title/AUR_helpers).
Thanks to [mhdi](https://aur.archlinux.org/account/mhdi) for taking care of AUR package maintenance.
### NixOS
- Install via `nix-shell`
```Console
nix-shell -p libation
```
A `nix-shell` will temporarily modify your $PATH environment variable. This can be used to try a piece of software before deciding to permanently install it.
- Install via NixOS configuration
```Console
environment.systemPackages = [
pkgs.libation
];
```
Add the following Nix code to your NixOS Configuration, usually located in `/etc/nixos/configuration.nix`
- On NixOS via via `nix-env`
```Console
nix-env -iA nixos.libation
```
- On Non NixOS via `nix-env`
```Console
nix-env -iA nixpkgs.libation
```
Warning: Using `nix-env` permanently modifies a local profile of installed packages. This must be updated and maintained by the user in the same way as with a traditional package manager.
Thanks to [TomaSajt](https://github.com/tomasajt) for taking care of Nix package maintenance.
If your desktop uses gtk, you should now see Libation among your applications. If your desktop uses gtk, you should now see Libation among your applications.
+45 -8
View File
@@ -1,9 +1,10 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Run Libation on MacOS # Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac. This walkthrough should get you up and running with Libation on your Mac.
@@ -12,16 +13,52 @@ This walkthrough should get you up and running with Libation on your Mac.
## Install Libation ## Install Libation
- Download the file from the latest release and extract it. - Download the file from the latest release and extract it.
- Apple Silicon (M1, M2, ...): `Libation.9.4.2-macOS-chardonnay-`**arm64**`.tgz` - Apple Silicon (M1, M2, ...): `Libation.x.x.x-macOS-chardonnay-`**arm64**`.tgz`
- Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz` - Intel: `Libation.x.x.x-macOS-chardonnay-`**x64**`.tgz`
- Move the extracted Libation app bundle to your applications folder. - Move the extracted Libation app bundle to your applications folder.
- Open a terminal (Go > Utilities > Terminal) - Right-click on Libation and then click on open
- Copy/paste/run the following command (you'll be prompted to enter your password) - The first time, it will not immediately show you an option to open it. Just dismiss the dialog and do the same thing again (right-click -> open) then you will get an option to run the unsigned application. This takes about 10 seconds.
## If this doesn't work
You can add Libation as a safe app without touching Gatekeeper.
- Copy/paste/run the following command. Adjust the file path to the Libation.app on your computer if necessary.
```Console
xattr -r -d com.apple.quarantine ~/Downloads/Libation.app
```
- Close the terminal and use Libation!
## If this still doesn't work
- Copy/paste/run the following command (you'll be prompted to enter your Mac password)
```Console ```Console
sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable sudo spctl --master-disable && sudo spctl --add --label "Libation" /Applications/Libation.app && open /Applications/Libation.app && sudo spctl --master-enable
``` ```
- Close the terminal and use Libation!
* Close the terminal and use Libation!
## "Apple can't check app for malicious software"
From: [How to Open Anyway](https://support.apple.com/guide/mac-help/apple-cant-check-app-for-malicious-software-mchleab3a043/mac):
* On your Mac, choose Apple menu > System Settings, then click Privacy & Security in the sidebar. (You may need to scroll down.)
* Go to Security, then click Open.
* Click Open Anyway. This button is available for about an hour after you try to open the app.
* Enter your login password, then click OK.
## Troubleshooting
If Libation fails to start after completing the above steps, try the following:
1. Right-click the Libation app in your applications folder and select _Show Package Contents_
2. Open the `Contents` folder and then the `MacOS` folder.
3. Find the file named `Libation`, right-click it, and then select _Open_.
Libation _should_ launch, and you should now be able to open Libation by just double-clicking the app bundle in your applications folder.
## Running Hangover ## Running Hangover
@@ -30,7 +67,7 @@ Libation comes with a recovery app called Hangover. You can start it by running
open /Applications/Libation.app --args hangover open /Applications/Libation.app --args hangover
``` ```
## Runnign LibationCli ## Running LibationCli
Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command: Libation comes with a command-line interface. Unfortunately, due to the way apps are sandboxed on mac, its use is somewhat limited. To open a new sandboxed terminal in LibationCli's directory, run the following command:
```Console ```Console
+7
View File
@@ -1,3 +1,10 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Naming Templates # Naming Templates
File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options. File and Folder names can be customized using Libation's built-in tag template naming engine. To edit how folder and file names are created, go to Settings \> Download/Decrypt and edit the naming templates. If you're splitting your audiobook into multiple files by chapter, you can also use a custom template to set each chapter's title metadata tag by editing the template in Settings \> Audio File Options.
+1 -1
View File
@@ -1,6 +1,6 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
+38 -27
View File
@@ -1,28 +1,39 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" enable-background="new 0 0 512 512"> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 524 524" enable-background="new 0 0 524 524">
<path id="glass" d= <defs>
"M139,2 <g id="glass">
A 192,200 0 0 0 103,84 <path fill-rule="evenodd" d=
A 222,334 41 0 0 241,320 "M262,8
V478 h-117
H160 a 192,200 0 0 0 -36,82
A 16,16 0 0 0 160,510 a 222,334 41 0 0 138,236
H352 v158
A16 16 0 0 0 352,478 h-81
H271 a 16,16 0 0 0 0,32
V320 h192
A 222,334 -41 0 0 409,84 a 16 16 0 0 0 0,-32
A 192,200 0 0 0 373,2 h-81
M355,32 v-158
A 192,200 0 0 1 381,127 a 222,334 -41 0 0 138,-236
A 187.5,334 -35 0 1 256,286 a 192,200 0 0 0 -36,-82
A 187.5,334 35 0 1 131,127 h-117
A 192,200 0 0 1 157,32 m-99,30
H355 a 192,200 0 0 0 -26,95
z" /> a 187.5,334 35 0 0 125,159
a 187.5,334 -35 0 0 125,-159
<path id="wine-level" d= a 192,200 0 0 0 -26,-95
"M146,128 h-198
A 168,300 35 0 0 256,270 z"/>
A 168,300 -35 0 0 366,128 </g>
z"/> <g id="wine-level">
<path d=
"M158,136
a 168,305 35 0 0 104,136
a 168,305 -35 0 0 104,-136
z"/>
</g>
</defs>
<use href="#glass" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
<use href="#wine-level" stroke="#ffffffa0" stroke-width="16" fill="Transparent" />
<use href="#glass" fill="Black" />
<use href="#wine-level" fill="Black" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 968 B

+4 -1
View File
@@ -2,9 +2,11 @@
## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest) ## [Download Libation](https://github.com/rmcrackan/Libation/releases/latest)
### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/MBucari?locale.x=en_us) ### If you found this useful, tell a friend. If you found this REALLY useful, you can click here to [PayPal.me](https://paypal.me/mcrackan?locale.x=en_us)
...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**. ...or just tell more friends. As long as I'm maintaining this software, it will remain **free** and **open source**.
# Table of Contents # Table of Contents
- [Audible audiobook manager](#audible-audiobook-manager) - [Audible audiobook manager](#audible-audiobook-manager)
@@ -31,6 +33,7 @@
- [Custom File Naming](Documentation/NamingTemplates.md) - [Custom File Naming](Documentation/NamingTemplates.md)
- [Command Line Interface](Documentation/Advanced.md#command-line-interface) - [Command Line Interface](Documentation/Advanced.md#command-line-interface)
- [Docker](Documentation/Docker.md) - [Docker](Documentation/Docker.md)
- [Frequently Asked Questions](Documentation/FrequentlyAskedQuestions.md)
## Getting started ## Getting started
-3
View File
@@ -109,9 +109,6 @@ ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then if ! grep -q 'fs.inotify.max_user_instances=524288' /etc/sysctl.conf; then
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
fi fi
# workaround until this file is moved to the user's home directory
touch /usr/lib/libation/appsettings.json
chmod 666 /usr/lib/libation/appsettings.json
" >> $FOLDER_DEBIAN/postinst " >> $FOLDER_DEBIAN/postinst
echo "Creating control file..." echo "Creating control file..."
+5 -1
View File
@@ -86,8 +86,12 @@ delfiles=( 'libmp3lame.arm64.so' 'libmp3lame.x64.so' 'libmp3lame.x64.dll' 'libmp
if [[ "$ARCH" == "arm64" ]] if [[ "$ARCH" == "arm64" ]]
then then
delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib') delfiles+=('libmp3lame.x64.dylib' 'ffmpegaac.x64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.arm64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.arm64.dylib $BUNDLE_MACOS/libmp3lame.dylib
else else
delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib') delfiles+=('libmp3lame.arm64.dylib' 'ffmpegaac.arm64.dylib')
mv $BUNDLE_MACOS/ffmpegaac.x64.dylib $BUNDLE_MACOS/ffmpegaac.dylib
mv $BUNDLE_MACOS/libmp3lame.x64.dylib $BUNDLE_MACOS/libmp3lame.dylib
fi fi
@@ -111,4 +115,4 @@ mv $APP_FILE ./bundle/$APP_FILE
rm -r $BUNDLE rm -r $BUNDLE
echo "Done!" echo "Done!"
-2
View File
@@ -94,8 +94,6 @@ install * %{buildroot}%{_libdir}/%{name}/
if [ \$1 -eq 1 ] ; then if [ \$1 -eq 1 ] ; then
# Initial installation # Initial installation
touch %{_libdir}/%{name}/appsettings.json
chmod 666 %{_libdir}/%{name}/appsettings.json
ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation ln -s %{_libdir}/%{name}/Libation %{_bindir}/libation
ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover ln -s %{_libdir}/%{name}/Hangover %{_bindir}/hangover
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -13,7 +13,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="1.1.0" /> <PackageReference Include="AAXClean.Codecs" Version="1.1.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+9 -5
View File
@@ -1,12 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<Version>10.6.3.1</Version> <Version>12.0.1.1</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Octokit" Version="6.0.0" /> <PackageReference Include="Octokit" Version="14.0.0" />
<PackageReference Include="Serilog.Sinks.ZipFile" Version="1.0.1" /> <!-- Do not remove unused Serilog.Sinks -->
<!-- Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use. -->
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.ZipFile" Version="3.1.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" /> <ProjectReference Include="..\ApplicationServices\ApplicationServices.csproj" />
@@ -19,4 +23,4 @@
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DebugType>embedded</DebugType> <DebugType>embedded</DebugType>
</PropertyGroup> </PropertyGroup>
</Project> </Project>
+171 -15
View File
@@ -1,21 +1,22 @@
using System; using ApplicationServices;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using ApplicationServices;
using AudibleUtilities; using AudibleUtilities;
using Dinah.Core;
using Dinah.Core.IO; using Dinah.Core.IO;
using Dinah.Core.Logging; using Dinah.Core.Logging;
using LibationFileManager; using LibationFileManager;
using System.Runtime.InteropServices; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog; using Serilog;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
namespace AppScaffolding namespace AppScaffolding
{ {
public enum ReleaseIdentifier public enum ReleaseIdentifier
{ {
None, None,
WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64, WindowsClassic = OS.Windows | Variety.Classic | Architecture.X64,
@@ -87,6 +88,9 @@ namespace AppScaffolding
// //
Migrations.migrate_to_v6_6_9(config); Migrations.migrate_to_v6_6_9(config);
Migrations.migrate_to_v11_5_0(config);
Migrations.migrate_to_v11_6_5(config);
Migrations.migrate_to_v12_0_1(config);
} }
/// <summary>Initialize logging. Wire-up events. Run after migration</summary> /// <summary>Initialize logging. Wire-up events. Run after migration</summary>
@@ -124,6 +128,9 @@ namespace AppScaffolding
{ "MinimumLevel", "Information" }, { "MinimumLevel", "Information" },
{ "WriteTo", new JArray { "WriteTo", new JArray
{ {
// ABOUT SINKS
// Only ZipFile sink is currently used. By user request (June 2024) others packages are included for experimental use.
// new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved // new JObject { {"Name", "Console" } }, // this has caused more problems than it's solved
new JObject new JObject
{ {
@@ -227,12 +234,20 @@ namespace AppScaffolding
// begin logging session with a form feed // begin logging session with a form feed
Log.Logger.Information("\r\n\f"); Log.Logger.Information("\r\n\f");
Log.Logger.Information("Begin. {@DebugInfo}", new
static int fileCount(FileManager.LongPath longPath)
{
try { return FileManager.FileUtility.SaferEnumerateFiles(longPath).Count(); }
catch { return -1; }
}
Log.Logger.Information("Begin. {@DebugInfo}", new
{ {
AppName = EntryAssembly.GetName().Name, AppName = EntryAssembly.GetName().Name,
Version = BuildVersion.ToString(), Version = BuildVersion.ToString(),
ReleaseIdentifier, ReleaseIdentifier,
Configuration.OS, Configuration.OS,
Environment.OSVersion,
InteropFactory.InteropFunctionsType, InteropFactory.InteropFunctionsType,
Mode = mode, Mode = mode,
LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(), LogLevel_Verbose_Enabled = Log.Logger.IsVerboseEnabled(),
@@ -242,6 +257,7 @@ namespace AppScaffolding
LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(), LogLevel_Error_Enabled = Log.Logger.IsErrorEnabled(),
LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(), LogLevel_Fatal_Enabled = Log.Logger.IsFatalEnabled(),
config.AutoScan,
config.BetaOptIn, config.BetaOptIn,
config.UseCoverAsFolderIcon, config.UseCoverAsFolderIcon,
config.LibationFiles, config.LibationFiles,
@@ -250,10 +266,12 @@ namespace AppScaffolding
config.InProgress, config.InProgress,
AudibleFileStorage.DownloadsInProgressDirectory, AudibleFileStorage.DownloadsInProgressDirectory,
DownloadsInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DownloadsInProgressDirectory).Count(), DownloadsInProgressFiles = fileCount(AudibleFileStorage.DownloadsInProgressDirectory),
AudibleFileStorage.DecryptInProgressDirectory, AudibleFileStorage.DecryptInProgressDirectory,
DecryptInProgressFiles = FileManager.FileUtility.SaferEnumerateFiles(AudibleFileStorage.DecryptInProgressDirectory).Count(), DecryptInProgressFiles = fileCount(AudibleFileStorage.DecryptInProgressDirectory),
disableIPv6 = AppContext.TryGetSwitch("System.Net.DisableIPv6", out bool disableIPv6Value),
}); });
if (InteropFactory.InteropFunctionsType is null) if (InteropFactory.InteropFunctionsType is null)
@@ -262,8 +280,11 @@ namespace AppScaffolding
private static void wireUpSystemEvents(Configuration configuration) private static void wireUpSystemEvents(Configuration configuration)
{ {
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex(); LibraryCommands.LibrarySizeChanged += (object _, List<DataLayer.LibraryBook> libraryBooks)
LibraryCommands.BookUserDefinedItemCommitted += (_, books) => SearchEngineCommands.UpdateBooks(books); => SearchEngineCommands.FullReIndex(libraryBooks);
LibraryCommands.BookUserDefinedItemCommitted += (_, books)
=> SearchEngineCommands.UpdateBooks(books);
} }
public static UpgradeProperties GetLatestRelease() public static UpgradeProperties GetLatestRelease()
@@ -390,5 +411,140 @@ namespace AppScaffolding
UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails"); UNSAFE_MigrationHelper.Settings_AddUniqueToArray("Serilog.Enrich", "WithExceptionDetails");
} }
} }
}
class FilterState_6_6_9
{
public bool UseDefault { get; set; }
public List<string> Filters { get; set; } = new();
}
public static void migrate_to_v12_0_1(Configuration config)
{
#nullable enable
//Migrate from version 1 file cache to the dictionary-based version 2 cache
const string FILENAME_V1 = "FileLocations.json";
const string FILENAME_V2 = "FileLocationsV2.json";
var jsonFileV1 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V1);
var jsonFileV2 = Path.Combine(Configuration.Instance.LibationFiles, FILENAME_V2);
if (!File.Exists(jsonFileV2) && File.Exists(jsonFileV1))
{
try
{
//FilePathCache loads the cache in its static constructor,
//so perform migration without using FilePathCache.CacheEntry
if (JArray.Parse(File.ReadAllText(jsonFileV1)) is not JArray v1Cache || v1Cache.Count == 0)
return;
Dictionary<string, JArray> cache = new();
//Convert to c# objects to speed up searching by ID inside the iterator
var allItems
= v1Cache
.Select(i => new
{
Id = i["Id"]?.Value<string>(),
Path = i["Path"]?["Path"]?.Value<string>()
}).Where(i => i.Id != null)
.ToArray();
foreach (var id in allItems.Select(i => i.Id).OfType<string>().Distinct())
{
//Use this opportunity to purge non-existent files and re-classify file types
//(due to *.aax files previously not being classified as FileType.AAXC)
var items = allItems
.Where(i => i.Id == id && File.Exists(i.Path))
.Select(i => new JObject
{
{ "Id", i.Id },
{ "FileType", (int)FileTypes.GetFileTypeFromPath(i.Path) },
{ "Path", new JObject{ { "Path", i.Path } } }
})
.ToArray();
if (items.Length == 0)
continue;
cache[id] = new JArray(items);
}
var cacheJson = new JObject { { "Dictionary", JObject.FromObject(cache) } };
var cacheFileText = cacheJson.ToString(Formatting.Indented);
void migrate()
{
File.WriteAllText(jsonFileV2, cacheFileText);
File.Delete(jsonFileV1);
}
try { migrate(); }
catch (IOException)
{
try { migrate(); }
catch (IOException)
{
migrate();
}
}
}
catch { /* eat */ }
}
#nullable restore
}
public static void migrate_to_v11_6_5(Configuration config)
{
//Settings migration for unsupported sample rates (#1116)
if (config.MaxSampleRate < AAXClean.SampleRate.Hz_8000)
config.MaxSampleRate = AAXClean.SampleRate.Hz_8000;
else if (config.MaxSampleRate > AAXClean.SampleRate.Hz_48000)
config.MaxSampleRate = AAXClean.SampleRate.Hz_48000;
}
public static void migrate_to_v11_5_0(Configuration config)
{
// Read file, but convert old format to new (with Name field) as necessary.
if (!File.Exists(QuickFilters.JsonFile))
{
QuickFilters.InMemoryState = new();
return;
}
try
{
if (JsonConvert.DeserializeObject<QuickFilters.FilterState>(File.ReadAllText(QuickFilters.JsonFile))
is QuickFilters.FilterState inMemState)
{
QuickFilters.InMemoryState = inMemState;
return;
}
}
catch
{
// Eat
}
try
{
if (JsonConvert.DeserializeObject<FilterState_6_6_9>(File.ReadAllText(QuickFilters.JsonFile))
is FilterState_6_6_9 inMemState)
{
// Copy old structure to new.
QuickFilters.InMemoryState = new();
QuickFilters.InMemoryState.UseDefault = inMemState.UseDefault;
foreach (var oldFilter in inMemState.Filters)
QuickFilters.InMemoryState.Filters.Add(new QuickFilters.NamedFilter(oldFilter, null));
return;
}
Debug.Assert(false, "Should not get here, QuickFilters.json deserialization issue");
}
catch
{
// Eat
}
}
}
} }
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CsvHelper" Version="30.0.1" /> <PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="NPOI" Version="2.6.1" /> <PackageReference Include="NPOI" Version="2.7.2" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+43 -30
View File
@@ -119,9 +119,16 @@ namespace ApplicationServices
logTime($"pre {nameof(scanAccountsAsync)} all"); logTime($"pre {nameof(scanAccountsAsync)} all");
var libraryOptions = new LibraryOptions var libraryOptions = new LibraryOptions
{ {
ResponseGroups = LibraryOptions.ResponseGroupOptions.ALL_OPTIONS, ResponseGroups
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215 = LibraryOptions.ResponseGroupOptions.Rating | LibraryOptions.ResponseGroupOptions.Media
| LibraryOptions.ResponseGroupOptions.Relationships | LibraryOptions.ResponseGroupOptions.ProductDesc
| LibraryOptions.ResponseGroupOptions.Contributors | LibraryOptions.ResponseGroupOptions.ProvidedReview
| LibraryOptions.ResponseGroupOptions.ProductPlans | LibraryOptions.ResponseGroupOptions.Series
| LibraryOptions.ResponseGroupOptions.CategoryLadders | LibraryOptions.ResponseGroupOptions.ProductExtendedAttrs
| LibraryOptions.ResponseGroupOptions.PdfUrl | LibraryOptions.ResponseGroupOptions.OriginAsin
| LibraryOptions.ResponseGroupOptions.IsFinished,
ImageSizes = LibraryOptions.ImageSizeOptions._500 | LibraryOptions.ImageSizeOptions._1215
}; };
var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions); var importItems = await scanAccountsAsync(apiExtendedfunc, accounts, libraryOptions);
logTime($"post {nameof(scanAccountsAsync)} all"); logTime($"post {nameof(scanAccountsAsync)} all");
@@ -215,7 +222,7 @@ namespace ApplicationServices
{ {
int qtyChanged = await Task.Run(() => SaveContext(context)); int qtyChanged = await Task.Run(() => SaveContext(context));
if (qtyChanged > 0) if (qtyChanged > 0)
await Task.Run(finalizeLibrarySizeChange); await Task.Run(() => finalizeLibrarySizeChange(context));
return qtyChanged; return qtyChanged;
} }
catch (Exception ex) catch (Exception ex)
@@ -322,7 +329,7 @@ namespace ApplicationServices
// this is any changes at all to the database, not just new books // this is any changes at all to the database, not just new books
if (qtyChanges > 0) if (qtyChanges > 0)
await Task.Run(() => finalizeLibrarySizeChange()); await Task.Run(() => finalizeLibrarySizeChange(context));
logTime("importIntoDbAsync -- post finalizeLibrarySizeChange"); logTime("importIntoDbAsync -- post finalizeLibrarySizeChange");
return newCount; return newCount;
@@ -362,16 +369,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() NoTracking entities before SaveChanges()
foreach (var lb in removeLibraryBooks) foreach (var lb in removeLibraryBooks)
{ {
lb.IsDeleted = true; lb.IsDeleted = true;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} }
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@@ -391,16 +398,16 @@ namespace ApplicationServices
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() NoTracking entities before SaveChanges()
foreach (var lb in libraryBooks) foreach (var lb in libraryBooks)
{ {
lb.IsDeleted = false; lb.IsDeleted = false;
context.Attach(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified; context.Entry(lb).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} }
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@@ -425,7 +432,7 @@ namespace ApplicationServices
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
finalizeLibrarySizeChange(); finalizeLibrarySizeChange(context);
return qtyChanges; return qtyChanges;
} }
@@ -438,10 +445,14 @@ namespace ApplicationServices
#endregion #endregion
// call this whenever books are added or removed from library // call this whenever books are added or removed from library
private static void finalizeLibrarySizeChange() => LibrarySizeChanged?.Invoke(null, null); private static void finalizeLibrarySizeChange(LibationContext context)
{
var library = context.GetLibrary_Flat_NoTracking(includeParents: true);
LibrarySizeChanged?.Invoke(null, library);
}
/// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary> /// <summary>Occurs when the size of the library changes. ie: books are added or removed</summary>
public static event EventHandler LibrarySizeChanged; public static event EventHandler<List<LibraryBook>> LibrarySizeChanged;
/// <summary> /// <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. /// 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.
@@ -511,17 +522,18 @@ namespace ApplicationServices
if (libraryBooks is null || !libraryBooks.Any()) if (libraryBooks is null || !libraryBooks.Any())
return 0; return 0;
foreach (var book in libraryBooks)
action?.Invoke(book.Book.UserDefinedItem);
using var context = DbContexts.GetContext(); using var context = DbContexts.GetContext();
// Attach() NoTracking entities before SaveChanges() // Entry() instead of Attach() due to possible stack overflow with large tables
foreach (var book in libraryBooks) foreach (var book in libraryBooks)
{ {
context.Attach(book.Book.UserDefinedItem).State = Microsoft.EntityFrameworkCore.EntityState.Modified; action?.Invoke(book.Book.UserDefinedItem);
context.Attach(book.Book.UserDefinedItem.Rating).State = Microsoft.EntityFrameworkCore.EntityState.Modified;
} var udiEntity = context.Entry(book.Book.UserDefinedItem);
udiEntity.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
udiEntity.Reference(udi => udi.Rating).TargetEntry.State = Microsoft.EntityFrameworkCore.EntityState.Modified;
}
var qtyChanges = context.SaveChanges(); var qtyChanges = context.SaveChanges();
if (qtyChanges > 0) if (qtyChanges > 0)
@@ -592,7 +604,8 @@ namespace ApplicationServices
var results = libraryBooks var results = libraryBooks
.AsParallel() .AsParallel()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) }) .WithoutParents()
.Select(lb => new { absent = lb.AbsentFromLastScan, status = Liberated_Status(lb.Book) })
.ToList(); .ToList();
var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated); var booksFullyBackedUp = results.Count(r => r.status == LiberatedStatus.Liberated);
@@ -115,6 +115,9 @@ namespace ApplicationServices
[Name("LastDownloadedVersion")] [Name("LastDownloadedVersion")]
public string LastDownloadedVersion { get; set; } public string LastDownloadedVersion { get; set; }
[Name("IsFinished")]
public bool IsFinished { get; set; }
} }
public static class LibToDtos public static class LibToDtos
{ {
@@ -141,7 +144,7 @@ namespace ApplicationServices
PictureId = a.Book.PictureId, PictureId = a.Book.PictureId,
IsAbridged = a.Book.IsAbridged, IsAbridged = a.Book.IsAbridged,
DatePublished = a.Book.DatePublished, DatePublished = a.Book.DatePublished,
CategoriesNames = a.Book.LowestCategoryNames().Any() ? a.Book.LowestCategoryNames().Aggregate((a, b) => $"{a}, {b}") : "", CategoriesNames = string.Join("; ", a.Book.LowestCategoryNames()),
MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating, MyRatingOverall = a.Book.UserDefinedItem.Rating.OverallRating,
MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating, MyRatingPerformance = a.Book.UserDefinedItem.Rating.PerformanceRating,
MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating, MyRatingStory = a.Book.UserDefinedItem.Rating.StoryRating,
@@ -153,8 +156,8 @@ namespace ApplicationServices
Language = a.Book.Language, Language = a.Book.Language,
LastDownloaded = a.Book.UserDefinedItem.LastDownloaded, LastDownloaded = a.Book.UserDefinedItem.LastDownloaded,
LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "", LastDownloadedVersion = a.Book.UserDefinedItem.LastDownloadedVersion?.ToString() ?? "",
IsFinished = a.Book.UserDefinedItem.IsFinished
}).ToList(); }).ToList();
} }
public static class LibraryExporter public static class LibraryExporter
{ {
@@ -229,6 +232,7 @@ namespace ApplicationServices
nameof(ExportDto.Language), nameof(ExportDto.Language),
nameof(ExportDto.LastDownloaded), nameof(ExportDto.LastDownloaded),
nameof(ExportDto.LastDownloadedVersion), nameof(ExportDto.LastDownloadedVersion),
nameof(ExportDto.IsFinished)
}; };
var col = 0; var col = 0;
foreach (var c in columns) foreach (var c in columns)
@@ -306,6 +310,7 @@ namespace ApplicationServices
} }
row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion); row.CreateCell(++col).SetCellValue(dto.LastDownloadedVersion);
row.CreateCell(++col).SetCellValue(dto.IsFinished);
rowIndex++; rowIndex++;
} }
@@ -48,6 +48,8 @@ namespace ApplicationServices
} }
public static void FullReIndex() => performSafeCommand(fullReIndex); public static void FullReIndex() => performSafeCommand(fullReIndex);
public static void FullReIndex(List<LibraryBook> libraryBooks)
=> performSafeCommand(se => fullReIndex(se, libraryBooks.WithoutParents()));
internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e => internal static void UpdateUserDefinedItems(LibraryBook book) => performSafeCommand(e =>
{ {
@@ -94,8 +96,11 @@ namespace ApplicationServices
private static void fullReIndex(SearchEngine engine) private static void fullReIndex(SearchEngine engine)
{ {
var library = DbContexts.GetLibrary_Flat_NoTracking(); var library = DbContexts.GetLibrary_Flat_NoTracking();
engine.CreateNewIndex(library); fullReIndex(engine, library);
} }
private static void fullReIndex(SearchEngine engine, IEnumerable<LibraryBook> libraryBooks)
=> engine.CreateNewIndex(libraryBooks);
#endregion #endregion
} }
} }
+5 -1
View File
@@ -207,7 +207,11 @@ namespace AudibleUtilities
try try
{ {
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.ALL_OPTIONS); var items = await Api.GetCatalogProductsAsync(asins, CatalogOptions.ResponseGroupOptions.Rating | CatalogOptions.ResponseGroupOptions.Media
| CatalogOptions.ResponseGroupOptions.Relationships | CatalogOptions.ResponseGroupOptions.ProductDesc
| CatalogOptions.ResponseGroupOptions.Contributors | CatalogOptions.ResponseGroupOptions.ProvidedReview
| CatalogOptions.ResponseGroupOptions.ProductPlans | CatalogOptions.ResponseGroupOptions.Series
| CatalogOptions.ResponseGroupOptions.CategoryLadders | CatalogOptions.ResponseGroupOptions.ProductExtendedAttrs);
sw.Stop(); sw.Stop();
Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms"); Serilog.Log.Logger.Debug($"Batch {batchNum} End: Retrieved {items.Count} items in {sw.ElapsedMilliseconds} ms");
@@ -90,8 +90,10 @@ namespace AudibleUtilities
var distinct = items.GetSeriesDistinct(); var distinct = items.GetSeriesDistinct();
if (distinct.Any(s => s.SeriesId is null)) if (distinct.Any(s => s.SeriesId is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items))); exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesId)}", nameof(items)));
if (distinct.Any(s => s.SeriesName is null))
exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items))); //// unfortunately, a user has a series with no name
//if (distinct.Any(s => s.SeriesName is null))
// exceptions.Add(new ArgumentException($"Collection contains {nameof(Item.Series)} with null {nameof(Series.SeriesName)}", nameof(items)));
return exceptions; return exceptions;
} }
@@ -1,11 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="AudibleApi" Version="8.4.2.1" /> <PackageReference Include="AudibleApi" Version="9.3.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+6 -6
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
@@ -10,14 +10,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="7.2.3.1" /> <PackageReference Include="Dinah.Core" Version="9.0.0.1" />
<PackageReference Include="Dinah.EntityFrameworkCore" Version="7.1.1.1" /> <PackageReference Include="Dinah.EntityFrameworkCore" Version="9.0.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
+7 -3
View File
@@ -106,6 +106,7 @@ namespace DataLayer
ReplaceAuthors(authors); ReplaceAuthors(authors);
ReplaceNarrators(narrators); ReplaceNarrators(narrators);
} }
public void UpdateTitle(string title, string subtitle) public void UpdateTitle(string title, string subtitle)
{ {
Title = title?.Trim() ?? ""; Title = title?.Trim() ?? "";
@@ -113,8 +114,11 @@ namespace DataLayer
_titleWithSubtitle = null; _titleWithSubtitle = null;
} }
#region contributors, authors, narrators public void UpdateLengthInMinutes(int lengthInMinutes)
internal HashSet<BookContributor> ContributorsLink { get; private set; } => LengthInMinutes = lengthInMinutes;
#region contributors, authors, narrators
internal HashSet<BookContributor> ContributorsLink { get; private set; }
public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList(); public IEnumerable<Contributor> Authors => ContributorsLink.ByRole(Role.Author).Select(bc => bc.Contributor).ToList();
public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList(); public IEnumerable<Contributor> Narrators => ContributorsLink.ByRole(Role.Narrator).Select(bc => bc.Contributor).ToList();
@@ -187,7 +191,7 @@ namespace DataLayer
ArgumentValidator.EnsureNotNull(ladders, nameof(ladders)); ArgumentValidator.EnsureNotNull(ladders, nameof(ladders));
//Replace all existing category ladders. //Replace all existing category ladders.
//Some books make have duplocate ladders //Some books make have duplicate ladders
CategoriesLink.Clear(); CategoriesLink.Clear();
CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l))); CategoriesLink.UnionWith(ladders.Distinct().Select(l => new BookCategory(this, l)));
} }
+17 -1
View File
@@ -195,7 +195,23 @@ namespace DataLayer
} }
} }
} }
#endregion #endregion
#region IsFinished
private bool _isFinished;
public bool IsFinished
{
get => _isFinished;
set
{
if (value != _isFinished)
{
_isFinished = value;
OnItemChanged(nameof(IsFinished));
}
}
}
#endregion
public override string ToString() => $"{Book} {Rating} {Tags}"; public override string ToString() => $"{Book} {Rating} {Tags}";
} }
+2 -1
View File
@@ -6,6 +6,7 @@ namespace DataLayer
public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext> public class LibationContextFactory : DesignTimeDbContextFactoryBase<LibationContext>
{ {
protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options); protected override LibationContext CreateNewInstance(DbContextOptions<LibationContext> options) => new LibationContext(options);
protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString) => optionsBuilder.UseSqlite(connectionString); protected override void UseDatabaseEngine(DbContextOptionsBuilder optionsBuilder, string connectionString)
=> optionsBuilder.UseSqlite(connectionString, ob => ob.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery));
} }
} }
@@ -0,0 +1,468 @@
// <auto-generated />
using System;
using DataLayer;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DataLayer.Migrations
{
[DbContext(typeof(LibationContext))]
[Migration("20240911114741_MyComment")]
partial class MyComment
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.Property<int>("_categoriesCategoryId")
.HasColumnType("INTEGER");
b.Property<int>("_categoryLaddersCategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("_categoriesCategoryId", "_categoryLaddersCategoryLadderId");
b.HasIndex("_categoryLaddersCategoryLadderId");
b.ToTable("CategoryCategoryLadder");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Property<int>("BookId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleProductId")
.HasColumnType("TEXT");
b.Property<int>("ContentType")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DatePublished")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasColumnType("TEXT");
b.Property<bool>("IsAbridged")
.HasColumnType("INTEGER");
b.Property<string>("Language")
.HasColumnType("TEXT");
b.Property<int>("LengthInMinutes")
.HasColumnType("INTEGER");
b.Property<string>("Locale")
.HasColumnType("TEXT");
b.Property<string>("PictureId")
.HasColumnType("TEXT");
b.Property<string>("PictureLarge")
.HasColumnType("TEXT");
b.Property<string>("Subtitle")
.HasColumnType("TEXT");
b.Property<string>("Title")
.HasColumnType("TEXT");
b.Property<long>("_audioFormat")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.HasIndex("AudibleProductId");
b.ToTable("Books");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("CategoryLadderId")
.HasColumnType("INTEGER");
b.HasKey("BookId", "CategoryLadderId");
b.HasIndex("BookId");
b.HasIndex("CategoryLadderId");
b.ToTable("BookCategory");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<int>("ContributorId")
.HasColumnType("INTEGER");
b.Property<int>("Role")
.HasColumnType("INTEGER");
b.Property<byte>("Order")
.HasColumnType("INTEGER");
b.HasKey("BookId", "ContributorId", "Role");
b.HasIndex("BookId");
b.HasIndex("ContributorId");
b.ToTable("BookContributor");
});
modelBuilder.Entity("DataLayer.Category", b =>
{
b.Property<int>("CategoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleCategoryId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("CategoryId");
b.HasIndex("AudibleCategoryId");
b.ToTable("Categories");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Property<int>("CategoryLadderId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.HasKey("CategoryLadderId");
b.ToTable("CategoryLadders");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Property<int>("ContributorId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleContributorId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("ContributorId");
b.HasIndex("Name");
b.ToTable("Contributors");
b.HasData(
new
{
ContributorId = -1,
Name = ""
});
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<bool>("AbsentFromLastScan")
.HasColumnType("INTEGER");
b.Property<string>("Account")
.HasColumnType("TEXT");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<bool>("IsDeleted")
.HasColumnType("INTEGER");
b.HasKey("BookId");
b.ToTable("LibraryBooks");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Property<int>("SeriesId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AudibleSeriesId")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.HasKey("SeriesId");
b.HasIndex("AudibleSeriesId");
b.ToTable("Series");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.Property<int>("SeriesId")
.HasColumnType("INTEGER");
b.Property<int>("BookId")
.HasColumnType("INTEGER");
b.Property<string>("Order")
.HasColumnType("TEXT");
b.HasKey("SeriesId", "BookId");
b.HasIndex("BookId");
b.HasIndex("SeriesId");
b.ToTable("SeriesBook");
});
modelBuilder.Entity("CategoryCategoryLadder", b =>
{
b.HasOne("DataLayer.Category", null)
.WithMany()
.HasForeignKey("_categoriesCategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", null)
.WithMany()
.HasForeignKey("_categoryLaddersCategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.OwnsOne("DataLayer.Rating", "Rating", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<float>("OverallRating")
.HasColumnType("REAL");
b1.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b1.Property<float>("StoryRating")
.HasColumnType("REAL");
b1.HasKey("BookId");
b1.ToTable("Books");
b1.WithOwner()
.HasForeignKey("BookId");
});
b.OwnsMany("DataLayer.Supplement", "Supplements", b1 =>
{
b1.Property<int>("SupplementId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<string>("Url")
.HasColumnType("TEXT");
b1.HasKey("SupplementId");
b1.HasIndex("BookId");
b1.ToTable("Supplement");
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.Navigation("Book");
});
b.OwnsOne("DataLayer.UserDefinedItem", "UserDefinedItem", b1 =>
{
b1.Property<int>("BookId")
.HasColumnType("INTEGER");
b1.Property<int>("BookStatus")
.HasColumnType("INTEGER");
b1.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT");
b1.Property<string>("LastDownloadedVersion")
.HasColumnType("TEXT");
b1.Property<int?>("PdfStatus")
.HasColumnType("INTEGER");
b1.Property<string>("Tags")
.HasColumnType("TEXT");
b1.HasKey("BookId");
b1.ToTable("UserDefinedItem", (string)null);
b1.WithOwner("Book")
.HasForeignKey("BookId");
b1.OwnsOne("DataLayer.Rating", "Rating", b2 =>
{
b2.Property<int>("UserDefinedItemBookId")
.HasColumnType("INTEGER");
b2.Property<float>("OverallRating")
.HasColumnType("REAL");
b2.Property<float>("PerformanceRating")
.HasColumnType("REAL");
b2.Property<float>("StoryRating")
.HasColumnType("REAL");
b2.HasKey("UserDefinedItemBookId");
b2.ToTable("UserDefinedItem");
b2.WithOwner()
.HasForeignKey("UserDefinedItemBookId");
});
b1.Navigation("Book");
b1.Navigation("Rating");
});
b.Navigation("Rating");
b.Navigation("Supplements");
b.Navigation("UserDefinedItem");
});
modelBuilder.Entity("DataLayer.BookCategory", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("CategoriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.CategoryLadder", "CategoryLadder")
.WithMany("BooksLink")
.HasForeignKey("CategoryLadderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("CategoryLadder");
});
modelBuilder.Entity("DataLayer.BookContributor", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("ContributorsLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Contributor", "Contributor")
.WithMany("BooksLink")
.HasForeignKey("ContributorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Contributor");
});
modelBuilder.Entity("DataLayer.LibraryBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithOne()
.HasForeignKey("DataLayer.LibraryBook", "BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
});
modelBuilder.Entity("DataLayer.SeriesBook", b =>
{
b.HasOne("DataLayer.Book", "Book")
.WithMany("SeriesLink")
.HasForeignKey("BookId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("DataLayer.Series", "Series")
.WithMany("BooksLink")
.HasForeignKey("SeriesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Book");
b.Navigation("Series");
});
modelBuilder.Entity("DataLayer.Book", b =>
{
b.Navigation("CategoriesLink");
b.Navigation("ContributorsLink");
b.Navigation("SeriesLink");
});
modelBuilder.Entity("DataLayer.CategoryLadder", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Contributor", b =>
{
b.Navigation("BooksLink");
});
modelBuilder.Entity("DataLayer.Series", b =>
{
b.Navigation("BooksLink");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DataLayer.Migrations
{
/// <inheritdoc />
public partial class MyComment : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsFinished",
table: "UserDefinedItem",
type: "INTEGER",
nullable: false,
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsFinished",
table: "UserDefinedItem");
}
}
}
@@ -15,7 +15,7 @@ namespace DataLayer.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.HasAnnotation("ProductVersion", "8.0.5");
modelBuilder.Entity("CategoryCategoryLadder", b => modelBuilder.Entity("CategoryCategoryLadder", b =>
{ {
@@ -312,6 +312,9 @@ namespace DataLayer.Migrations
b1.Property<int>("BookStatus") b1.Property<int>("BookStatus")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b1.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b1.Property<DateTime?>("LastDownloaded") b1.Property<DateTime?>("LastDownloaded")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
+5 -1
View File
@@ -44,7 +44,11 @@ namespace DataLayer
public static bool IsEpisodeParent(this Book book) public static bool IsEpisodeParent(this Book book)
=> book.ContentType is ContentType.Parent; => book.ContentType is ContentType.Parent;
public static bool HasLiberated(this Book book)
public static IEnumerable<LibraryBook> WithoutParents(this IEnumerable<LibraryBook> libraryBooks)
=> libraryBooks.Where(lb => !lb.Book.IsEpisodeParent());
public static bool HasLiberated(this Book book)
=> book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated || => book.UserDefinedItem.BookStatus is LiberatedStatus.Liberated ||
book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated; book.UserDefinedItem.PdfStatus is not null and LiberatedStatus.Liberated;
} }
@@ -21,8 +21,8 @@ namespace DataLayer
.AsNoTrackingWithIdentityResolution() .AsNoTrackingWithIdentityResolution()
.GetLibrary() .GetLibrary()
.AsEnumerable() .AsEnumerable()
.Where(lb => !lb.Book.IsEpisodeParent() || includeParents) .Where(c => !c.Book.IsEpisodeParent() || includeParents)
.ToList(); .ToList();
public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId) public static LibraryBook GetLibraryBook_Flat_NoTracking(this LibationContext context, string productId)
=> context => context
@@ -91,7 +91,7 @@ namespace DataLayer
} }
#nullable disable #nullable disable
public static IEnumerable<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent) public static List<LibraryBook> FindChildren(this IEnumerable<LibraryBook> bookList, LibraryBook parent)
=> bookList => bookList
.Where( .Where(
lb => lb =>
@@ -149,6 +149,8 @@ namespace DtoImporterService
{ {
var item = importItem.DtoItem; var item = importItem.DtoItem;
book.UpdateLengthInMinutes(item.LengthInMinutes);
// Update the book titles, since formatting can change // Update the book titles, since formatting can change
book.UpdateTitle(item.Title, item.Subtitle); book.UpdateTitle(item.Title, item.Subtitle);
@@ -162,6 +164,9 @@ namespace DtoImporterService
if (item.PictureLarge is not null) if (item.PictureLarge is not null)
book.PictureLarge = item.PictureLarge; book.PictureLarge = item.PictureLarge;
if (item.IsFinished is not null)
book.UserDefinedItem.IsFinished = item.IsFinished.Value;
// 2023-02-01 // 2023-02-01
// updateBook must update language on books which were imported before the migration which added language. // updateBook must update language on books which were imported before the migration which added language.
// Can eventually delete this // Can eventually delete this
@@ -23,9 +23,11 @@ namespace DtoImporterService
loadLocal_categories(); loadLocal_categories();
// upsert // upsert
//Import item may not have no (null) categories
var categoryLadders = importItems var categoryLadders = importItems
.Where(i => i.DtoItem.CategoryLadders is not null)
.SelectMany(i => i.DtoItem.CategoryLadders) .SelectMany(i => i.DtoItem.CategoryLadders)
.Select(cl => cl.Ladder) .Select(cl => cl?.Ladder)
.Where(l => l?.Length > 0) .Where(l => l?.Length > 0)
.ToList(); .ToList();
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+2 -1
View File
@@ -21,7 +21,8 @@ namespace FileLiberator
var series = libraryBook.Book.SeriesLink.SingleOrDefault(); var series = libraryBook.Book.SeriesLink.SingleOrDefault();
if (series is not null) if (series is not null)
{ {
var seriesParent = ApplicationServices.DbContexts.GetContext().GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId); using var context = ApplicationServices.DbContexts.GetContext();
var seriesParent = context.GetLibraryBook_Flat_NoTracking(series.Series.AudibleSeriesId);
if (seriesParent is not null) if (seriesParent is not null)
{ {
+5
View File
@@ -60,6 +60,11 @@ namespace FileLiberator
config.LameMatchSourceBR, config.LameMatchSourceBR,
chapters); chapters);
if (m4bBook.AppleTags.Tracks is (int trackNum, int trackCount))
{
lameConfig.ID3.Track = trackCount > 0 ? $"{trackNum}/{trackCount}" : trackNum.ToString();
}
using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite); using var mp3File = File.Open(Path.GetTempFileName(), FileMode.OpenOrCreate, FileAccess.ReadWrite);
try try
{ {
+9 -2
View File
@@ -375,8 +375,15 @@ namespace FileLiberator
{ {
if (Configuration.Instance.AllowLibationFixup) if (Configuration.Instance.AllowLibationFixup)
{ {
e = OnRequestCoverArt(); try
abDownloader.SetCoverArt(e); {
e = OnRequestCoverArt();
abDownloader.SetCoverArt(e);
}
catch (Exception ex)
{
Serilog.Log.Logger.Error(ex, "Failed to retrieve cover art from server.");
}
} }
if (e is not null) if (e is not null)
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+1 -1
View File
@@ -30,7 +30,7 @@ namespace FileLiberator
using var persister = AudibleApiStorage.GetAccountsSettingsPersister(); using var persister = AudibleApiStorage.GetAccountsSettingsPersister();
var nickname var nickname
= persister.AccountsSettings.Accounts = persister.AccountsSettings.Accounts
.FirstOrDefault(a => a.AccountId == libraryBook.Account) .FirstOrDefault(a => a.AccountId == libraryBook.Account && a.Locale.Name == libraryBook.Book.Locale)
?.AccountName; ?.AccountName;
return new() return new()
+22 -9
View File
@@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace FileManager namespace FileManager
{ {
/// <summary> /// <summary>
@@ -16,9 +17,9 @@ namespace FileManager
public string SearchPattern { get; private set; } public string SearchPattern { get; private set; }
public SearchOption SearchOption { get; private set; } public SearchOption SearchOption { get; private set; }
private FileSystemWatcher fileSystemWatcher { get; set; } private FileSystemWatcher? fileSystemWatcher { get; set; }
private BlockingCollection<FileSystemEventArgs> directoryChangesEvents { get; set; } private BlockingCollection<FileSystemEventArgs>? directoryChangesEvents { get; set; }
private Task backgroundScanner { get; set; } private Task? backgroundScanner { get; set; }
private object fsCacheLocker { get; } = new(); private object fsCacheLocker { get; } = new();
private List<LongPath> fsCache { get; } = new(); private List<LongPath> fsCache { get; } = new();
@@ -32,7 +33,7 @@ namespace FileManager
Init(); Init();
} }
public LongPath FindFile(System.Text.RegularExpressions.Regex regex) public LongPath? FindFile(System.Text.RegularExpressions.Regex regex)
{ {
lock (fsCacheLocker) lock (fsCacheLocker)
return fsCache.FirstOrDefault(s => regex.IsMatch(s)); return fsCache.FirstOrDefault(s => regex.IsMatch(s));
@@ -49,7 +50,7 @@ namespace FileManager
lock (fsCacheLocker) lock (fsCacheLocker)
{ {
fsCache.Clear(); fsCache.Clear();
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption)); fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
} }
} }
@@ -58,7 +59,7 @@ namespace FileManager
Stop(); Stop();
lock (fsCacheLocker) lock (fsCacheLocker)
fsCache.AddRange(FileUtility.SaferEnumerateFiles(RootDirectory, SearchPattern, SearchOption)); fsCache.AddRange(SafestEnumerateFiles(RootDirectory));
directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>(); directoryChangesEvents = new BlockingCollection<FileSystemEventArgs>();
fileSystemWatcher = new FileSystemWatcher(RootDirectory) fileSystemWatcher = new FileSystemWatcher(RootDirectory)
@@ -105,13 +106,13 @@ namespace FileManager
private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e) private void FileSystemWatcher_Changed(object sender, FileSystemEventArgs e)
{ {
directoryChangesEvents.Add(e); directoryChangesEvents?.Add(e);
} }
#region Background Thread #region Background Thread
private void BackgroundScanner() private void BackgroundScanner()
{ {
while (directoryChangesEvents.TryTake(out FileSystemEventArgs change, -1)) while (directoryChangesEvents?.TryTake(out var change, -1) is true)
{ {
lock (fsCacheLocker) lock (fsCacheLocker)
UpdateLocalCache(change); UpdateLocalCache(change);
@@ -151,11 +152,23 @@ namespace FileManager
if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path)) if (Path.GetFileName(path).Contains("LibationContext.db") || !File.Exists(path) && !Directory.Exists(path))
return; return;
if (File.GetAttributes(path).HasFlag(FileAttributes.Directory)) if (File.GetAttributes(path).HasFlag(FileAttributes.Directory))
AddUniqueFiles(FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption)); AddUniqueFiles(SafestEnumerateFiles(path));
else else
AddUniqueFile(path); AddUniqueFile(path);
} }
private IEnumerable<LongPath> SafestEnumerateFiles(string path)
{
try
{
return FileUtility.SaferEnumerateFiles(path, SearchPattern, SearchOption);
}
catch
{
return [];
}
}
private void AddUniqueFiles(IEnumerable<LongPath> newFiles) private void AddUniqueFiles(IEnumerable<LongPath> newFiles)
{ {
foreach (var file in newFiles) foreach (var file in newFiles)
+3 -3
View File
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dinah.Core" Version="7.2.3.1" /> <PackageReference Include="Dinah.Core" Version="9.0.0.1" />
<PackageReference Include="Polly" Version="7.2.4" /> <PackageReference Include="Polly" Version="8.5.2" />
</ItemGroup> </ItemGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+14 -9
View File
@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -7,20 +8,20 @@ using Dinah.Core;
using Polly; using Polly;
using Polly.Retry; using Polly.Retry;
#nullable enable
namespace FileManager namespace FileManager
{ {
public static class FileUtility public static class FileUtility
{ {
/// <summary> /// <summary>
/// "txt" => ".txt" /// "txt" => ".txt"
/// <br />".txt" => ".txt" /// <br />".txt" => ".txt"
/// <br />null or whitespace => "" /// <br />null or whitespace => ""
/// </summary> /// </summary>
public static string GetStandardizedExtension(string extension) [return: NotNull]
public static string GetStandardizedExtension(string? extension)
=> string.IsNullOrWhiteSpace(extension) => string.IsNullOrWhiteSpace(extension)
? (extension ?? "")?.Trim() ? string.Empty
: '.' + extension.Trim().Trim('.'); : '.' + extension.Trim().Trim('.');
/// <summary> /// <summary>
@@ -48,18 +49,18 @@ namespace FileManager
/// <br/>- ensure uniqueness /// <br/>- ensure uniqueness
/// <br/>- enforce max file length /// <br/>- enforce max file length
/// </summary> /// </summary>
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false) public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string? fileExtension, bool returnFirstExisting = false)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension)); ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
fileExtension = GetStandardizedExtension(fileExtension); fileExtension = GetStandardizedExtension(fileExtension);
// remove invalid chars // remove invalid chars
path = GetSafePath(path, replacements); path = GetSafePath(path, replacements);
// ensure uniqueness and check lengths // ensure uniqueness and check lengths
var dir = Path.GetDirectoryName(path); var dir = Path.GetDirectoryName(path)?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
var fileName = Path.GetFileName(path); var fileName = Path.GetFileName(path);
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase); var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
@@ -84,6 +85,7 @@ namespace FileManager
public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements) public static LongPath GetSafePath(LongPath path, ReplacementCharacters replacements)
{ {
ArgumentValidator.EnsureNotNull(path, nameof(path)); ArgumentValidator.EnsureNotNull(path, nameof(path));
ArgumentValidator.EnsureNotNull(replacements, nameof(replacements));
var pathNoPrefix = path.PathWithoutPrefix; var pathNoPrefix = path.PathWithoutPrefix;
@@ -159,7 +161,7 @@ namespace FileManager
LongPath source, LongPath source,
LongPath destination, LongPath destination,
ReplacementCharacters replacements, ReplacementCharacters replacements,
string extension = null, string? extension = null,
bool overwrite = false) bool overwrite = false)
{ {
extension ??= Path.GetExtension(source); extension ??= Path.GetExtension(source);
@@ -213,6 +215,9 @@ namespace FileManager
SaferDelete(destination); SaferDelete(destination);
var dir = Path.GetDirectoryName(destination); var dir = Path.GetDirectoryName(destination);
if (dir is null)
throw new DirectoryNotFoundException();
Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir }); Serilog.Log.Logger.Debug("Attempt to create directory: {@DebugText}", new { dir });
Directory.CreateDirectory(dir); Directory.CreateDirectory(dir);
+9 -6
View File
@@ -8,9 +8,10 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
#nullable enable
namespace FileManager namespace FileManager
{ {
public sealed class LogArchiver : IAsyncDisposable public sealed class LogArchiver : IAsyncDisposable, IDisposable
{ {
public Encoding Encoding { get; set; } public Encoding Encoding { get; set; }
public string FileName { get; } public string FileName { get; }
@@ -39,28 +40,28 @@ namespace FileManager
e.Delete(); e.Delete();
} }
public async Task AddFileAsync(string name, JObject contents, string comment = null) public async Task AddFileAsync(string name, JObject contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(contents, nameof(contents)); ArgumentValidator.EnsureNotNull(contents, nameof(contents));
await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment); await AddFileAsync(name, Encoding.GetBytes(contents.ToString(Newtonsoft.Json.Formatting.Indented)), comment);
} }
public async Task AddFileAsync(string name, string contents, string comment = null) public async Task AddFileAsync(string name, string contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(contents, nameof(contents)); ArgumentValidator.EnsureNotNull(contents, nameof(contents));
await AddFileAsync(name, Encoding.GetBytes(contents), comment); await AddFileAsync(name, Encoding.GetBytes(contents), comment);
} }
public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string comment = null) public Task AddFileAsync(string name, ReadOnlyMemory<byte> contents, string? comment = null)
{ {
ArgumentValidator.EnsureNotNull(name, nameof(name)); ArgumentValidator.EnsureNotNull(name, nameof(name));
name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name); name = ReplacementCharacters.Barebones.ReplaceFilenameChars(name);
return Task.Run(() => AddfileInternal(name, contents.Span, comment)); return Task.Run(() => AddFileInternal(name, contents.Span, comment));
} }
private readonly object lockObj = new(); private readonly object lockObj = new();
private void AddfileInternal(string name, ReadOnlySpan<byte> contents, string comment) private void AddFileInternal(string name, ReadOnlySpan<byte> contents, string? comment)
{ {
lock (lockObj) lock (lockObj)
{ {
@@ -73,5 +74,7 @@ namespace FileManager
} }
public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose); public async ValueTask DisposeAsync() => await Task.Run(archive.Dispose);
public void Dispose() => archive.Dispose();
} }
} }
+15 -13
View File
@@ -1,9 +1,11 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
#nullable enable
namespace FileManager namespace FileManager
{ {
public class LongPath public class LongPath
@@ -15,9 +17,9 @@ namespace FileManager
public static readonly int MaxPathLength; public static readonly int MaxPathLength;
private const int WIN_MAX_PATH = 260; private const int WIN_MAX_PATH = 260;
private const string WIN_LONG_PATH_PREFIX = @"\\?\"; private const string WIN_LONG_PATH_PREFIX = @"\\?\";
internal static readonly bool IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows); internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
internal static readonly bool IsLinux = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Linux); internal static readonly bool IsLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
internal static readonly bool IsOSX = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.OSX); internal static readonly bool IsOSX = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
public string Path { get; } public string Path { get; }
@@ -60,7 +62,8 @@ namespace FileManager
=> IsWindows ? filename.Length => IsWindows ? filename.Length
: Encoding.UTF8.GetByteCount(filename); : Encoding.UTF8.GetByteCount(filename);
public static implicit operator LongPath(string path) [return: NotNullIfNotNull(nameof(path))]
public static implicit operator LongPath?(string? path)
{ {
if (path is null) return null; if (path is null) return null;
@@ -93,7 +96,8 @@ namespace FileManager
} }
} }
public static implicit operator string(LongPath path) => path?.Path; [return: NotNullIfNotNull(nameof(path))]
public static implicit operator string?(LongPath? path) => path?.Path;
[JsonIgnore] [JsonIgnore]
public string ShortPathName public string ShortPathName
@@ -127,8 +131,6 @@ namespace FileManager
//for newly-created entries in ther file system. Existing entries made while //for newly-created entries in ther file system. Existing entries made while
//8dot3 names were disabled will not be reachable by short paths. //8dot3 names were disabled will not be reachable by short paths.
if (Path is null) return null;
StringBuilder shortPathBuffer = new(MaxPathLength); StringBuilder shortPathBuffer = new(MaxPathLength);
GetShortPathName(Path, shortPathBuffer, MaxPathLength); GetShortPathName(Path, shortPathBuffer, MaxPathLength);
return shortPathBuffer.ToString(); return shortPathBuffer.ToString();
@@ -141,7 +143,6 @@ namespace FileManager
get get
{ {
if (!IsWindows) return Path; if (!IsWindows) return Path;
if (Path is null) return null;
StringBuilder longPathBuffer = new(MaxPathLength); StringBuilder longPathBuffer = new(MaxPathLength);
GetLongPathName(Path, longPathBuffer, MaxPathLength); GetLongPathName(Path, longPathBuffer, MaxPathLength);
@@ -156,17 +157,18 @@ namespace FileManager
{ {
if (!IsWindows) return Path; if (!IsWindows) return Path;
return return
Path?.StartsWith(WIN_LONG_PATH_PREFIX) == true ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length) Path.StartsWith(WIN_LONG_PATH_PREFIX)
:Path; ? Path.Remove(0, WIN_LONG_PATH_PREFIX.Length)
: Path;
} }
} }
public override string ToString() => Path; public override string ToString() => Path;
public override int GetHashCode() => Path.GetHashCode(); public override int GetHashCode() => Path.GetHashCode();
public override bool Equals(object obj) => obj is LongPath other && Path == other.Path; public override bool Equals(object? obj) => obj is LongPath other && Path == other.Path;
public static bool operator ==(LongPath path1, LongPath path2) => path1.Equals(path2); public static bool operator ==(LongPath? path1, LongPath? path2) => path1?.Equals(path2) is true;
public static bool operator !=(LongPath path1, LongPath path2) => !path1.Equals(path2); public static bool operator !=(LongPath? path1, LongPath? path2) => path1 is null || path2 is null || !path1.Equals(path2);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
@@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
internal interface IClosingPropertyTag : IPropertyTag internal interface IClosingPropertyTag : IPropertyTag
@@ -17,7 +19,7 @@ internal interface IClosingPropertyTag : IPropertyTag
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param> /// <param name="propertyTag">The registered <see cref="IPropertyTag"/></param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag); bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag);
} }
public class ConditionalTagCollection<TClass> : TagCollection public class ConditionalTagCollection<TClass> : TagCollection
@@ -37,6 +39,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
private class ConditionalTag : TagBase, IClosingPropertyTag private class ConditionalTag : TagBase, IClosingPropertyTag
{ {
public override Regex NameMatcher { get; }
public Regex NameCloseMatcher { get; } public Regex NameCloseMatcher { get; }
public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression) public ConditionalTag(ITemplateTag templateTag, RegexOptions options, Expression conditionExpression)
@@ -46,7 +49,7 @@ public class ConditionalTagCollection<TClass> : TagCollection
NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options); NameCloseMatcher = new Regex($"^<-{templateTag.TagName}>", options);
} }
public bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag propertyTag) public bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? propertyTag)
{ {
var match = NameCloseMatcher.Match(templateString); var match = NameCloseMatcher.Match(templateString);
if (match.Success) if (match.Success)
@@ -1,19 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
public class NamingTemplate public class NamingTemplate
{ {
public string TemplateText { get; private set; } public string TemplateText { get; private set; } = string.Empty;
public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse; public IEnumerable<ITemplateTag> TagsInUse => _tagsInUse;
public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName); public IEnumerable<ITemplateTag> TagsRegistered => TagCollections.SelectMany(t => t).DistinctBy(t => t.TagName);
public IEnumerable<string> Warnings => errors.Concat(warnings); public IEnumerable<string> Warnings => errors.Concat(warnings);
public IEnumerable<string> Errors => errors; public IEnumerable<string> Errors => errors;
private Delegate templateToString; private Delegate? templateToString;
private readonly List<string> warnings = new(); private readonly List<string> warnings = new();
private readonly List<string> errors = new(); private readonly List<string> errors = new();
private readonly IEnumerable<TagCollection> TagCollections; private readonly IEnumerable<TagCollection> TagCollections;
@@ -30,6 +32,9 @@ public class NamingTemplate
/// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param> /// <param name="propertyClasses">Instances of the TClass used in <see cref="PropertyTagCollection{TClass}"/> and <see cref="ConditionalTagCollection{TClass}"/></param>
public TemplatePart Evaluate(params object[] propertyClasses) public TemplatePart Evaluate(params object[] propertyClasses)
{ {
if (templateToString is null)
throw new InvalidOperationException();
// Match propertyClasses to the arguments required by templateToString.DynamicInvoke(). // Match propertyClasses to the arguments required by templateToString.DynamicInvoke().
// First parameter is "this", so ignore it. // First parameter is "this", so ignore it.
var delegateArgTypes = templateToString.Method.GetParameters().Skip(1); var delegateArgTypes = templateToString.Method.GetParameters().Skip(1);
@@ -39,7 +44,7 @@ public class NamingTemplate
if (args.Length != delegateArgTypes.Count()) if (args.Length != delegateArgTypes.Count())
throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}"); throw new ArgumentException($"This instance of {nameof(NamingTemplate)} requires the following arguments: {string.Join(", ", delegateArgTypes.Select(t => t.Name).Distinct())}");
return ((TemplatePart)templateToString.DynamicInvoke(args)).FirstPart; return (templateToString.DynamicInvoke(args) as TemplatePart)!.FirstPart;
} }
/// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary> /// <summary>Parse a template string to a <see cref="NamingTemplate"/></summary>
@@ -69,7 +74,7 @@ public class NamingTemplate
} }
/// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary> /// <summary>Builds an <see cref="Expression"/> tree that will evaluate to a <see cref="TemplatePart"/></summary>
private static Expression GetExpressionTree(BinaryNode node) private static Expression GetExpressionTree(BinaryNode? node)
{ {
if (node is null) return TemplatePart.Blank; if (node is null) return TemplatePart.Blank;
else if (node.IsValue) return node.Expression; else if (node.IsValue) return node.Expression;
@@ -81,10 +86,10 @@ public class NamingTemplate
} }
/// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary> /// <summary>Parse a template string into a <see cref="BinaryNode"/> tree</summary>
private BinaryNode IntermediateParse(string templateString) private BinaryNode IntermediateParse(string? templateString)
{ {
if (templateString is null) if (templateString is null)
throw new NullReferenceException(ERROR_NULL_IS_INVALID); throw new ArgumentException(ERROR_NULL_IS_INVALID);
else if (string.IsNullOrEmpty(templateString)) else if (string.IsNullOrEmpty(templateString))
warnings.Add(WARNING_EMPTY); warnings.Add(WARNING_EMPTY);
else if (string.IsNullOrWhiteSpace(templateString)) else if (string.IsNullOrWhiteSpace(templateString))
@@ -93,12 +98,12 @@ public class NamingTemplate
TemplateText = templateString; TemplateText = templateString;
BinaryNode topNode = BinaryNode.CreateRoot(); BinaryNode topNode = BinaryNode.CreateRoot();
BinaryNode currentNode = topNode; BinaryNode? currentNode = topNode;
List<char> literalChars = new(); List<char> literalChars = new();
while (templateString.Length > 0) while (templateString.Length > 0)
{ {
if (StartsWith(templateString, out string exactPropertyName, out var propertyTag, out var valueExpression)) if (StartsWith(templateString, out var exactPropertyName, out var propertyTag, out var valueExpression))
{ {
checkAndAddLiterals(); checkAndAddLiterals();
@@ -116,7 +121,7 @@ public class NamingTemplate
{ {
checkAndAddLiterals(); checkAndAddLiterals();
BinaryNode lastParenth = currentNode; BinaryNode? lastParenth = currentNode;
while (lastParenth?.IsConditional is false) while (lastParenth?.IsConditional is false)
lastParenth = lastParenth.Parent; lastParenth = lastParenth.Parent;
@@ -168,7 +173,7 @@ public class NamingTemplate
} }
} }
private bool StartsWith(string template, out string exactName, out IPropertyTag propertyTag, out Expression valueExpression) private bool StartsWith(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? valueExpression)
{ {
foreach (var pc in TagCollections) foreach (var pc in TagCollections)
{ {
@@ -182,7 +187,7 @@ public class NamingTemplate
return false; return false;
} }
private bool StartsWithClosing(string template, out string exactName, out IClosingPropertyTag closingPropertyTag) private bool StartsWithClosing(string template, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
{ {
foreach (var pc in TagCollections) foreach (var pc in TagCollections)
{ {
@@ -198,36 +203,36 @@ public class NamingTemplate
private class BinaryNode private class BinaryNode
{ {
public string Name { get; } public string Name { get; }
public BinaryNode Parent { get; private set; } public BinaryNode? Parent { get; private set; }
public BinaryNode RightChild { get; private set; } public BinaryNode? RightChild { get; private set; }
public BinaryNode LeftChild { get; private set; } public BinaryNode? LeftChild { get; private set; }
public Expression Expression { get; private init; } public Expression Expression { get; }
public bool IsConditional { get; private init; } = false; public bool IsConditional { get; private init; } = false;
public bool IsValue { get; private init; } = false; public bool IsValue { get; private init; } = false;
public static BinaryNode CreateRoot() => new("Root"); public static BinaryNode CreateRoot() => new("Root", Expression.Empty());
public static BinaryNode CreateValue(string literal) => new("Literal") public static BinaryNode CreateValue(string literal)
{ => new("Literal", TemplatePart.CreateLiteral(literal))
IsValue = true, {
Expression = TemplatePart.CreateLiteral(literal) IsValue = true
}; };
public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) public static BinaryNode CreateValue(ITemplateTag templateTag, Expression property)
{ => new(templateTag.TagName, TemplatePart.CreateProperty(templateTag, property))
IsValue = true, {
Expression = TemplatePart.CreateProperty(templateTag, property) IsValue = true
}; };
public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property) => new(templateTag.TagName) public static BinaryNode CreateConditional(ITemplateTag templateTag, Expression property)
{ => new(templateTag.TagName, property)
IsConditional = true, {
Expression = property IsConditional = true
}; };
private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right) private static BinaryNode CreateConcatenation(BinaryNode left, BinaryNode right)
{ {
var newNode = new BinaryNode("Concatenation") var newNode = new BinaryNode("Concatenation", Expression.Empty())
{ {
LeftChild = left, LeftChild = left,
RightChild = right RightChild = right
@@ -237,7 +242,12 @@ public class NamingTemplate
return newNode; return newNode;
} }
private BinaryNode(string name) => Name = name; private BinaryNode(string name, Expression expression)
{
Name = name;
Expression = expression;
}
public override string ToString() => Name; public override string ToString() => Name;
public BinaryNode AddNewNode(BinaryNode newNode) public BinaryNode AddNewNode(BinaryNode newNode)
@@ -5,6 +5,7 @@ using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString); public delegate string PropertyFormatter<T>(ITemplateTag templateTag, T value, string formatString);
@@ -37,7 +38,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default /// and a formatting string and returnes the value the formatted string. If <see cref="null"/>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param> /// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty> formatter = null) public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty?> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
where TProperty : struct where TProperty : struct
=> RegisterWithFormatter(templateTag, propertyGetter, formatter); => RegisterWithFormatter(templateTag, propertyGetter, formatter);
@@ -59,7 +60,7 @@ public class PropertyTagCollection<TClass> : TagCollection
/// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property /// <param name="formatter">Optional formatting function that accepts the <typeparamref name="TProperty"/> property
/// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default /// and a formatting string and returnes the value formatted to string. If <see cref="null"/>, use the default
/// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param> /// <typeparamref name="TProperty"/> formatter if present, or <see cref="object.ToString"/></param>
public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty> formatter = null) public void Add<TProperty>(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TProperty>? formatter = null)
=> RegisterWithFormatter(templateTag, propertyGetter, formatter); => RegisterWithFormatter(templateTag, propertyGetter, formatter);
/// <summary> /// <summary>
@@ -72,14 +73,15 @@ public class PropertyTagCollection<TClass> : TagCollection
=> RegisterWithToString(templateTag, propertyGetter, toString); => RegisterWithToString(templateTag, propertyGetter, toString);
private void RegisterWithFormatter<TProperty, TPropertyValue> private void RegisterWithFormatter<TProperty, TPropertyValue>
(ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue> formatter) (ITemplateTag templateTag, Func<TClass, TProperty> propertyGetter, PropertyFormatter<TPropertyValue>? formatter)
{ {
ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag)); ArgumentValidator.EnsureNotNull(templateTag, nameof(templateTag));
ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter)); ArgumentValidator.EnsureNotNull(propertyGetter, nameof(propertyGetter));
var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter); var expr = Expression.Call(Expression.Constant(propertyGetter.Target), propertyGetter.Method, Parameter);
formatter ??= GetDefaultFormatter<TPropertyValue>();
if ((formatter ??= GetDefaultFormatter<TPropertyValue>()) is null) if (formatter is null)
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc)); AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, ToStringFunc));
else else
AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter)); AddPropertyTag(new PropertyTag<TPropertyValue>(templateTag, Options, expr, formatter));
@@ -97,7 +99,7 @@ public class PropertyTagCollection<TClass> : TagCollection
private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? ""; private static string ToStringFunc<T>(T propertyValue) => propertyValue?.ToString() ?? "";
private PropertyFormatter<T> GetDefaultFormatter<T>() private PropertyFormatter<T>? GetDefaultFormatter<T>()
{ {
try try
{ {
@@ -109,6 +111,7 @@ public class PropertyTagCollection<TClass> : TagCollection
private class PropertyTag<TPropertyValue> : TagBase private class PropertyTag<TPropertyValue> : TagBase
{ {
public override Regex NameMatcher { get; }
private Func<Expression, string, Expression> CreateToStringExpression { get; } private Func<Expression, string, Expression> CreateToStringExpression { get; }
public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter) public PropertyTag(ITemplateTag templateTag, RegexOptions options, Expression propertyGetter, PropertyFormatter<TPropertyValue> formatter)
+5 -3
View File
@@ -1,7 +1,9 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
internal interface IPropertyTag internal interface IPropertyTag
@@ -22,13 +24,13 @@ internal interface IPropertyTag
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param> /// <param name="propertyValue">The <see cref="Expression"/> that returns the property's value</param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
bool StartsWith(string templateString, out string exactName, out Expression propertyValue); bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue);
} }
internal abstract class TagBase : IPropertyTag internal abstract class TagBase : IPropertyTag
{ {
public ITemplateTag TemplateTag { get; } public ITemplateTag TemplateTag { get; }
public Regex NameMatcher { get; protected init; } public abstract Regex NameMatcher { get; }
public Type ReturnType => ValueExpression.Type; public Type ReturnType => ValueExpression.Type;
protected Expression ValueExpression { get; } protected Expression ValueExpression { get; }
@@ -43,7 +45,7 @@ internal abstract class TagBase : IPropertyTag
/// <param name="formatter">The optional format string in the match inside the square brackets</param> /// <param name="formatter">The optional format string in the match inside the square brackets</param>
protected abstract Expression GetTagExpression(string exactName, string formatter); protected abstract Expression GetTagExpression(string exactName, string formatter);
public bool StartsWith(string templateString, out string exactName, out Expression propertyValue) public bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out Expression? propertyValue)
{ {
var match = NameMatcher.Match(templateString); var match = NameMatcher.Match(templateString);
if (match.Success) if (match.Success)
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
/// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary> /// <summary>A collection of <see cref="IPropertyTag"/>s registered to a single <see cref="Type"/>.</summary>
@@ -32,7 +34,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param> /// <param name="propertyValue">The <see cref="Expression"/> that returns the <paramref name="propertyTag"/>'s value</param>
/// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns> /// <returns>True if the <paramref name="templateString"/> starts with a tag registered in this class.</returns>
internal bool StartsWith(string templateString, out string exactName, out IPropertyTag propertyTag, out Expression propertyValue) internal bool StartsWith(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IPropertyTag? propertyTag, [NotNullWhen(true)] out Expression? propertyValue)
{ {
foreach (var p in PropertyTags) foreach (var p in PropertyTags)
{ {
@@ -57,7 +59,7 @@ public abstract class TagCollection : IEnumerable<ITemplateTag>
/// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param> /// <param name="exactName">The <paramref name="templateString"/> substring that was matched.</param>
/// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param> /// <param name="closingPropertyTag">The registered <see cref="IClosingPropertyTag"/></param>
/// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns> /// <returns>True if the <paramref name="templateString"/> starts with this tag.</returns>
internal bool StartsWithClosing(string templateString, out string exactName, out IClosingPropertyTag closingPropertyTag) internal bool StartsWithClosing(string templateString, [NotNullWhen(true)] out string? exactName, [NotNullWhen(true)] out IClosingPropertyTag? closingPropertyTag)
{ {
foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>()) foreach (var cg in PropertyTags.OfType<IClosingPropertyTag>())
{ {
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
#nullable enable
namespace FileManager.NamingTemplate; namespace FileManager.NamingTemplate;
/// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary> /// <summary>Represents one part of an evaluated <see cref="NamingTemplate"/>.</summary>
@@ -15,13 +16,13 @@ public class TemplatePart : IEnumerable<TemplatePart>
/// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is /// <summary> The <see cref="IPropertyTag"/>'s <see cref="ITemplateTag"/> if <see cref="TemplatePart"/> is
/// a registered property, otherwise <see cref="null"/> for string literals. </summary> /// a registered property, otherwise <see cref="null"/> for string literals. </summary>
public ITemplateTag TemplateTag { get; } public ITemplateTag? TemplateTag { get; }
/// <summary>The evaluated string.</summary> /// <summary>The evaluated string.</summary>
public string Value { get; } public string Value { get; }
private TemplatePart previous; private TemplatePart? previous;
private TemplatePart next; private TemplatePart? next;
private TemplatePart(string name, string value) private TemplatePart(string name, string value)
{ {
TagName = name; TagName = name;
@@ -53,14 +54,33 @@ public class TemplatePart : IEnumerable<TemplatePart>
private static Expression CreateExpression(string name, Expression value) private static Expression CreateExpression(string name, Expression value)
=> Expression.New(constructorInfo, Expression.Constant(name), value); => Expression.New(constructorInfo, Expression.Constant(name), value);
private static readonly ConstructorInfo constructorInfo private static readonly ConstructorInfo constructorInfo;
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(string) }); private static readonly ConstructorInfo tagTemplateConstructorInfo;
private static readonly MethodInfo addMethodInfo;
static TemplatePart()
{
var type = typeof(TemplatePart);
private static readonly ConstructorInfo tagTemplateConstructorInfo if (type.GetConstructor(
= typeof(TemplatePart).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(ITemplateTag), typeof(string) }); BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(string), typeof(string) }) is not ConstructorInfo c1)
throw new MissingMethodException(nameof(TemplatePart));
private static readonly MethodInfo addMethodInfo if (type.GetConstructor(
= typeof(TemplatePart).GetMethod(nameof(Concatenate), BindingFlags.NonPublic | BindingFlags.Static, new Type[] { typeof(TemplatePart), typeof(TemplatePart) }); BindingFlags.NonPublic | BindingFlags.Instance,
new Type[] { typeof(ITemplateTag), typeof(string) }) is not ConstructorInfo c2)
throw new MissingMethodException(nameof(TemplatePart));
if (type.GetMethod(
nameof(Concatenate),
BindingFlags.NonPublic | BindingFlags.Static,
new Type[] { typeof(TemplatePart), typeof(TemplatePart) }) is not MethodInfo m1)
throw new MissingMethodException(nameof(Concatenate));
constructorInfo = c1;
tagTemplateConstructorInfo = c2;
addMethodInfo = m1;
}
public IEnumerator<TemplatePart> GetEnumerator() public IEnumerator<TemplatePart> GetEnumerator()
{ {
+21 -18
View File
@@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
#nullable enable
namespace FileManager namespace FileManager
{ {
public class PersistentDictionary public class PersistentDictionary
@@ -13,19 +15,19 @@ namespace FileManager
public bool IsReadOnly { get; } public bool IsReadOnly { get; }
// optimize for strings. expectation is most settings will be strings and a rare exception will be something else // optimize for strings. expectation is most settings will be strings and a rare exception will be something else
private Dictionary<string, string> stringCache { get; } = new Dictionary<string, string>(); private Dictionary<string, string?> stringCache { get; } = new();
private Dictionary<string, object> objectCache { get; } = new Dictionary<string, object>(); private Dictionary<string, object?> objectCache { get; } = new();
public PersistentDictionary(string filepath, bool isReadOnly = false) public PersistentDictionary(string filepath, bool isReadOnly = false)
{ {
Filepath = filepath; Filepath = filepath;
IsReadOnly = isReadOnly; IsReadOnly = isReadOnly;
if (File.Exists(Filepath)) if (File.Exists(Filepath) || Path.GetDirectoryName(Filepath) is not string dirName)
return; return;
// will create any missing directories, incl subdirectories. if all already exist: no action // will create any missing directories, incl subdirectories. if all already exist: no action
Directory.CreateDirectory(Path.GetDirectoryName(filepath)); Directory.CreateDirectory(dirName);
if (IsReadOnly) if (IsReadOnly)
return; return;
@@ -33,13 +35,14 @@ namespace FileManager
createNewFile(); createNewFile();
} }
public string GetString(string propertyName, string defaultValue = null) [return: NotNullIfNotNull(nameof(defaultValue))]
public string? GetString(string propertyName, string? defaultValue = null)
{ {
if (!stringCache.ContainsKey(propertyName)) if (!stringCache.ContainsKey(propertyName))
{ {
var jObject = readFile(); var jObject = readFile();
if (jObject.ContainsKey(propertyName)) if (jObject.ContainsKey(propertyName))
stringCache[propertyName] = jObject[propertyName].Value<string>(); stringCache[propertyName] = jObject[propertyName]?.Value<string>();
else else
stringCache[propertyName] = defaultValue; stringCache[propertyName] = defaultValue;
} }
@@ -47,7 +50,8 @@ namespace FileManager
return stringCache[propertyName]; return stringCache[propertyName];
} }
public T GetNonString<T>(string propertyName, T defaultValue = default) [return: NotNullIfNotNull(nameof(defaultValue))]
public T? GetNonString<T>(string propertyName, T? defaultValue = default)
{ {
var obj = GetObject(propertyName); var obj = GetObject(propertyName);
@@ -72,21 +76,21 @@ namespace FileManager
throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}"); throw new InvalidCastException($"{obj.GetType()} is not convertible to {typeof(T)}");
} }
public object GetObject(string propertyName) public object? GetObject(string propertyName)
{ {
if (!objectCache.ContainsKey(propertyName)) if (!objectCache.ContainsKey(propertyName))
{ {
var jObject = readFile(); var jObject = readFile();
if (!jObject.ContainsKey(propertyName)) if (!jObject.ContainsKey(propertyName))
return null; return null;
objectCache[propertyName] = jObject[propertyName].Value<object>(); objectCache[propertyName] = jObject[propertyName]?.Value<object>();
} }
return objectCache[propertyName]; return objectCache[propertyName];
} }
public string GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}"); public string? GetStringFromJsonPath(string jsonPath, string propertyName) => GetStringFromJsonPath($"{jsonPath}.{propertyName}");
public string GetStringFromJsonPath(string jsonPath) public string? GetStringFromJsonPath(string jsonPath)
{ {
if (!stringCache.ContainsKey(jsonPath)) if (!stringCache.ContainsKey(jsonPath))
{ {
@@ -96,7 +100,7 @@ namespace FileManager
var token = jObject.SelectToken(jsonPath); var token = jObject.SelectToken(jsonPath);
if (token is null) if (token is null)
return null; return null;
stringCache[jsonPath] = (string)token; stringCache[jsonPath] = token.Value<string>();
} }
catch catch
{ {
@@ -110,7 +114,7 @@ namespace FileManager
public bool Exists(string propertyName) => readFile().ContainsKey(propertyName); public bool Exists(string propertyName) => readFile().ContainsKey(propertyName);
private object locker { get; } = new object(); private object locker { get; } = new object();
public void SetString(string propertyName, string newValue) public void SetString(string propertyName, string? newValue)
{ {
// only do this check in string cache, NOT object cache // only do this check in string cache, NOT object cache
if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue) if (stringCache.ContainsKey(propertyName) && stringCache[propertyName] == newValue)
@@ -122,7 +126,7 @@ namespace FileManager
writeFile(propertyName, newValue); writeFile(propertyName, newValue);
} }
public void SetNonString(string propertyName, object newValue) public void SetNonString(string propertyName, object? newValue)
{ {
// set cache // set cache
objectCache[propertyName] = newValue; objectCache[propertyName] = newValue;
@@ -160,7 +164,7 @@ namespace FileManager
return success; return success;
} }
private void writeFile(string propertyName, JToken newValue) private void writeFile(string propertyName, JToken? newValue)
{ {
if (IsReadOnly) if (IsReadOnly)
return; return;
@@ -190,7 +194,7 @@ namespace FileManager
/// <summary>WILL ONLY set if already present. WILL NOT create new</summary> /// <summary>WILL ONLY set if already present. WILL NOT create new</summary>
/// <returns>Value was changed</returns> /// <returns>Value was changed</returns>
public bool SetWithJsonPath(string jsonPath, string propertyName, string newValue, bool suppressLogging = false) public bool SetWithJsonPath(string jsonPath, string propertyName, string? newValue, bool suppressLogging = false)
{ {
if (IsReadOnly) if (IsReadOnly)
return false; return false;
@@ -242,7 +246,7 @@ namespace FileManager
return true; return true;
} }
private static string formatValueForLog(string value) private static string formatValueForLog(string? value)
=> value is null ? "[null]" => value is null ? "[null]"
: string.IsNullOrEmpty(value) ? "[empty]" : string.IsNullOrEmpty(value) ? "[empty]"
: string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]" : string.IsNullOrWhiteSpace(value) ? $"[whitespace. Length={value.Length}]"
@@ -283,7 +287,6 @@ namespace FileManager
private void createNewFile() private void createNewFile()
{ {
File.WriteAllText(Filepath, "{}"); File.WriteAllText(Filepath, "{}");
System.Threading.Thread.Sleep(100);
} }
} }
} }
+12 -8
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
#nullable enable
namespace FileManager namespace FileManager
{ {
public record Replacement public record Replacement
@@ -59,7 +60,7 @@ namespace FileManager
[JsonConverter(typeof(ReplacementCharactersConverter))] [JsonConverter(typeof(ReplacementCharactersConverter))]
public class ReplacementCharacters public class ReplacementCharacters
{ {
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count) if (obj is ReplacementCharacters second && Replacements.Count == second.Replacements.Count)
{ {
@@ -87,7 +88,7 @@ namespace FileManager
Replacement.OtherQuote(""), Replacement.OtherQuote(""),
Replacement.OpenAngleBracket(""), Replacement.OpenAngleBracket(""),
Replacement.CloseAngleBracket(""), Replacement.CloseAngleBracket(""),
Replacement.Colon(""), Replacement.Colon("_"),
Replacement.Asterisk("✱"), Replacement.Asterisk("✱"),
Replacement.QuestionMark(""), Replacement.QuestionMark(""),
Replacement.Pipe("⏐"), Replacement.Pipe("⏐"),
@@ -173,7 +174,7 @@ namespace FileManager
Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar
}).ToArray(); }).ToArray();
public IReadOnlyList<Replacement> Replacements { get; init; } required public IReadOnlyList<Replacement> Replacements { get; init; }
private string DefaultReplacement => Replacements[0].ReplacementString; private string DefaultReplacement => Replacements[0].ReplacementString;
private Replacement ForwardSlash => Replacements[1]; private Replacement ForwardSlash => Replacements[1];
private Replacement BackSlash => Replacements[2]; private Replacement BackSlash => Replacements[2];
@@ -298,12 +299,14 @@ namespace FileManager
public override bool CanConvert(Type objectType) public override bool CanConvert(Type objectType)
=> objectType == typeof(ReplacementCharacters); => objectType == typeof(ReplacementCharacters);
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
{ {
var jObj = JObject.Load(reader); var jObj = JObject.Load(reader);
var replaceArr = jObj[nameof(Replacement)]; var replaceArr = jObj[nameof(Replacement)];
IReadOnlyList<Replacement> dict = replaceArr var dict
.ToObject<Replacement[]>().ToList(); = replaceArr?.ToObject<Replacement[]>()?.ToList()
?? ReplacementCharacters.Default.Replacements;
//Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid. //Ensure that the first 6 replacements are for the expected chars and that all replacement strings are valid.
//If not, reset to default. //If not, reset to default.
@@ -325,9 +328,10 @@ namespace FileManager
return new ReplacementCharacters { Replacements = dict }; return new ReplacementCharacters { Replacements = dict };
} }
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{ {
ReplacementCharacters replacements = (ReplacementCharacters)value; if (value is not ReplacementCharacters replacements)
return;
var propertyNames = replacements.Replacements var propertyNames = replacements.Replacements
.Select(JObject.FromObject).ToList(); .Select(JObject.FromObject).ToList();
+14 -10
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 --> <!--Avalonia doesen't support TrimMode=link currently,but we are working on that https://github.com/AvaloniaUI/Avalonia/issues/6892 -->
<TrimMode>copyused</TrimMode> <TrimMode>copyused</TrimMode>
@@ -21,9 +21,13 @@
<ApplicationIcon>hangover.ico</ApplicationIcon> <ApplicationIcon>hangover.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <ItemGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</PropertyGroup> </ItemGroup>
<PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Avalonia\Debug</OutputPath> <OutputPath>..\bin\Avalonia\Debug</OutputPath>
@@ -67,13 +71,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.0" /> <PackageReference Include="Avalonia" Version="11.2.4" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" /> <PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.--> <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.0" /> <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\HangoverBase\HangoverBase.csproj" /> <ProjectReference Include="..\HangoverBase\HangoverBase.csproj" />
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir> <PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier> <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir> <PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier> <RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir> <PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup> </PropertyGroup>
@@ -2,7 +2,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net9.0-windows7.0</TargetFramework>
<EnableWindowsTargeting>true</EnableWindowsTargeting> <EnableWindowsTargeting>true</EnableWindowsTargeting>
<AssemblyName>Hangover</AssemblyName> <AssemblyName>Hangover</AssemblyName>
<UseWindowsForms>true</UseWindowsForms> <UseWindowsForms>true</UseWindowsForms>
@@ -15,6 +15,10 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup> </PropertyGroup>
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\classic</PublishDir> <PublishDir>..\bin\Publish\classic</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net9.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
+1
View File
@@ -6,6 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solution Items", "{03C8835F-936C-4AF7-87AE-FF92BDBE8B9B}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
REFERENCE.txt = REFERENCE.txt REFERENCE.txt = REFERENCE.txt
Upgrading dotnet version.txt = Upgrading dotnet version.txt
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt _ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt _AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
_DB_NOTES.txt = _DB_NOTES.txt _DB_NOTES.txt = _DB_NOTES.txt
+66 -3
View File
@@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:LibationAvalonia" xmlns:local="using:LibationAvalonia"
xmlns:controls="using:LibationAvalonia.Controls" xmlns:controls="using:LibationAvalonia.Controls"
xmlns:dialogs="using:LibationAvalonia.Dialogs"
x:Class="LibationAvalonia.App" x:Class="LibationAvalonia.App"
Name="Libation"> Name="Libation">
@@ -12,6 +13,10 @@
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ControlTheme x:Key="{x:Type TextBlock}" TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="Transparent" />
</ControlTheme>
<ResourceDictionary.ThemeDictionaries> <ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" /> <SolidColorBrush x:Key="SeriesEntryGridBackgroundBrush" Opacity="0.3" Color="#abffab" />
@@ -44,9 +49,9 @@
<SolidColorBrush x:Key="CancelRed" Color="#802727" /> <SolidColorBrush x:Key="CancelRed" Color="#802727" />
<SolidColorBrush x:Key="IconFill" Color="#DCE0DF" /> <SolidColorBrush x:Key="IconFill" Color="#DCE0DF" />
<SolidColorBrush x:Key="StoplightRed" Color="#5F0707" /> <SolidColorBrush x:Key="StoplightRed" Color="#7d1f1f" />
<SolidColorBrush x:Key="StoplightYellow" Color="#5F5B1A" /> <SolidColorBrush x:Key="StoplightYellow" Color="#7d7d1f" />
<SolidColorBrush x:Key="StoplightGreen" Color="#174E15" /> <SolidColorBrush x:Key="StoplightGreen" Color="#1f7d1f" />
<SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" /> <SolidColorBrush x:Key="DisabledGrayBrush" Opacity="0.4" Color="{StaticResource SystemChromeMediumColor}" />
@@ -77,6 +82,64 @@
<Style Selector="Button"> <Style Selector="Button">
<Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="VerticalContentAlignment" Value="Center"/>
</Style> </Style>
<Style Selector="ScrollBar">
<!-- It's called AutoHide, but this is really the mouseover shrink/expand. -->
<Setter Property="AllowAutoHide" Value="false"/>
</Style>
<Style Selector="dialogs|DialogWindow">
<Style Selector="^[UseCustomTitleBar=false]">
<Setter Property="SystemDecorations" Value="Full"/>
<Setter Property="Template">
<ControlTemplate>
<ContentPresenter Background="{DynamicResource SystemControlBackgroundAltHighBrush}" Content="{TemplateBinding Content}" />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="^[UseCustomTitleBar=true]">
<Setter Property="SystemDecorations" Value="BorderOnly"/>
<Setter Property="Template">
<ControlTemplate>
<Panel Background="{DynamicResource SystemControlBackgroundAltHighBrush}">
<Grid RowDefinitions="30,*">
<Border Name="DialogWindowTitleBorder" Margin="5,0" Background="{DynamicResource SystemAltMediumColor}">
<Border.Styles>
<Style Selector="Button#DialogCloseButton">
<Style Selector="^:pointerover">
<Style Selector="^ /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Red" />
</Style>
<Style Selector="^ Path">
<Setter Property="Fill" Value="{DynamicResource IconFill}" />
</Style>
</Style>
<Style Selector="^:not(:pointerover) /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
</Border.Styles>
<Grid ColumnDefinitions="Auto,*,Auto">
<Path Name="DialogWindowTitleIcon" Margin="3,5,0,5" Fill="{DynamicResource IconFill}" Stretch="Uniform" Data="{StaticResource LibationGlassIcon}"/>
<TextBlock Name="DialogWindowTitleTextBlock" Margin="8,0,0,0" VerticalAlignment="Center" FontWeight="DemiBold" FontSize="12" Grid.Column="1" Text="{TemplateBinding Title}" />
<Button Name="DialogCloseButton" Grid.Column="2">
<Path Fill="{DynamicResource SystemControlBackgroundBaseLowBrush}" VerticalAlignment="Center" Stretch="Uniform" RenderTransform="{StaticResource Rotate45Transform}" Data="{StaticResource CancelButtonIcon}" />
</Button>
</Grid>
</Border>
<Path Stroke="{DynamicResource SystemBaseMediumLowColor}" StrokeThickness="1" VerticalAlignment="Bottom" Stretch="Fill" Data="M0,0 L1,0" />
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}" />
</Grid>
</Panel>
</ControlTemplate>
</Setter>
</Style>
</Style>
</Application.Styles> </Application.Styles>
<NativeMenu.Menu> <NativeMenu.Menu>
+23 -18
View File
@@ -1,8 +1,6 @@
using ApplicationServices; using ApplicationServices;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
@@ -14,14 +12,13 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using ReactiveUI; using Avalonia.Threading;
using DataLayer;
namespace LibationAvalonia namespace LibationAvalonia
{ {
public class App : Application public class App : Application
{ {
public static Window MainWindow { get; private set; } public static MainWindow MainWindow { get; private set; }
public static IBrush ProcessQueueBookFailedBrush { get; private set; } public static IBrush ProcessQueueBookFailedBrush { get; private set; }
public static IBrush ProcessQueueBookCompletedBrush { get; private set; } public static IBrush ProcessQueueBookCompletedBrush { get; private set; }
public static IBrush ProcessQueueBookCancelledBrush { get; private set; } public static IBrush ProcessQueueBookCancelledBrush { get; private set; }
@@ -47,7 +44,7 @@ namespace LibationAvalonia
if (!config.LibationSettingsAreValid) if (!config.LibationSettingsAreValid)
{ {
var defaultLibationFilesDir = Configuration.UserProfile; var defaultLibationFilesDir = Configuration.DefaultLibationFilesDirectory;
// check for existing settings in default location // check for existing settings in default location
var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json"); var defaultSettingsFile = Path.Combine(defaultLibationFilesDir, "Settings.json");
@@ -85,19 +82,15 @@ namespace LibationAvalonia
// - error message, Exit() // - error message, Exit()
if (setupDialog.IsNewUser) if (setupDialog.IsNewUser)
{ {
Configuration.SetLibationFiles(Configuration.UserProfile); Configuration.SetLibationFiles(Configuration.DefaultLibationFilesDirectory);
setupDialog.Config.Books = Path.Combine(Configuration.UserProfile, nameof(Configuration.Books)); setupDialog.Config.Books = Configuration.DefaultBooksDirectory;
if (setupDialog.Config.LibationSettingsAreValid) if (setupDialog.Config.LibationSettingsAreValid)
{ {
var theme string theme = setupDialog.SelectedTheme.Content as string;
= setupDialog.SelectedTheme.Content is nameof(ThemeVariant.Dark)
? nameof(ThemeVariant.Dark)
: nameof(ThemeVariant.Light);
setupDialog.Config.SetString(theme, nameof(ThemeVariant)); setupDialog.Config.SetString(theme, nameof(ThemeVariant));
await RunMigrationsAsync(setupDialog.Config); await RunMigrationsAsync(setupDialog.Config);
LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists(); AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
@@ -181,7 +174,7 @@ namespace LibationAvalonia
if (continueResult == DialogResult.Yes) if (continueResult == DialogResult.Yes)
{ {
config.Books = Path.Combine(libationFilesDialog.SelectedDirectory, nameof(Configuration.Books)); config.Books = Configuration.DefaultBooksDirectory;
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
{ {
@@ -208,17 +201,29 @@ namespace LibationAvalonia
private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop) private static void ShowMainWindow(IClassicDesktopStyleApplicationLifetime desktop)
{ {
Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) is "Dark" ? ThemeVariant.Dark : ThemeVariant.Light; Current.RequestedThemeVariant = Configuration.Instance.GetString(propertyName: nameof(ThemeVariant)) switch
{
nameof(ThemeVariant.Dark) => ThemeVariant.Dark,
nameof(ThemeVariant.Light) => ThemeVariant.Light,
// "System"
_ => ThemeVariant.Default
};
//Reload colors for current theme //Reload colors for current theme
LoadStyles(); LoadStyles();
var mainWindow = new MainWindow(); var mainWindow = new MainWindow();
desktop.MainWindow = MainWindow = mainWindow; desktop.MainWindow = MainWindow = mainWindow;
mainWindow.OnLibraryLoaded(LibraryTask.GetAwaiter().GetResult()); mainWindow.Loaded += MainWindow_Loaded;
mainWindow.RestoreSizeAndLocation(Configuration.Instance); mainWindow.RestoreSizeAndLocation(Configuration.Instance);
mainWindow.Show(); mainWindow.Show();
} }
private static async void MainWindow_Loaded(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
var library = await LibraryTask;
await Dispatcher.UIThread.InvokeAsync(() => MainWindow.OnLibraryLoadedAsync(library));
}
private static void LoadStyles() private static void LoadStyles()
{ {
ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush)); ProcessQueueBookFailedBrush = AvaloniaUtils.GetBrushFromResources(nameof(ProcessQueueBookFailedBrush));
@@ -91,6 +91,32 @@
S 192,128 147,147 S 192,128 147,147
</StreamGeometry> </StreamGeometry>
<StreamGeometry x:Key="LibationGlassIcon">
M262,8
h-117
a 192,200 0 0 0 -36,82
a 222,334 41 0 0 138,236
v158
h-81
a 16,16 0 0 0 0,32
h192
a 16 16 0 0 0 0,-32
h-81
v-158
a 222,334 -41 0 0 138,-236
a 192,200 0 0 0 -36,-82
h-117
m-99,30
a 192,200 0 0 0 -26,95
a 187.5,334 35 0 0 125,159
a 187.5,334 -35 0 0 125,-159
a 192,200 0 0 0 -26,-95
h-198
M158,136
a 168,305 35 0 0 104,136
a 168,305 -35 0 0 104,-136
</StreamGeometry>
</ResourceDictionary> </ResourceDictionary>
</Styles.Resources> </Styles.Resources>
</Styles> </Styles>
@@ -107,7 +107,7 @@ namespace LibationAvalonia.Controls
if (known is Configuration.KnownDirectories.None) if (known is Configuration.KnownDirectories.None)
{ {
directoryState.CustomDir = noSubDir; directoryState.CustomDir = directory;
directoryState.CustomChecked = true; directoryState.CustomChecked = true;
} }
else else
@@ -51,7 +51,9 @@ namespace LibationAvalonia.Controls
{ {
Configuration.KnownDirectories.WinTemp, Configuration.KnownDirectories.WinTemp,
Configuration.KnownDirectories.UserProfile, Configuration.KnownDirectories.UserProfile,
Configuration.KnownDirectories.ApplicationData,
Configuration.KnownDirectories.AppDir, Configuration.KnownDirectories.AppDir,
Configuration.KnownDirectories.MyMusic,
Configuration.KnownDirectories.MyDocs, Configuration.KnownDirectories.MyDocs,
Configuration.KnownDirectories.LibationFiles Configuration.KnownDirectories.LibationFiles
}; };
@@ -166,16 +166,7 @@
MinWidth="80" MinWidth="80"
SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}" SelectedItem="{CompiledBinding ThemeVariant, Mode=TwoWay}"
ItemsSource="{CompiledBinding Themes}"/> ItemsSource="{CompiledBinding Themes}"/>
<TextBlock
Grid.Column="2"
FontSize="16"
FontWeight="Bold"
Margin="10,0"
VerticalAlignment="Center"
IsVisible="{CompiledBinding SelectionChanged}"
Text="Theme change takes effect on restart"/>
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>
@@ -1,4 +1,6 @@
using Avalonia.Controls; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using LibationFileManager; using LibationFileManager;
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -9,19 +11,98 @@ namespace LibationAvalonia.Dialogs
{ {
public bool SaveAndRestorePosition { get; set; } = true; public bool SaveAndRestorePosition { get; set; } = true;
public Control ControlToFocusOnShow { get; set; } public Control ControlToFocusOnShow { get; set; }
protected override Type StyleKeyOverride => typeof(DialogWindow);
public static readonly StyledProperty<bool> UseCustomTitleBarProperty =
AvaloniaProperty.Register<DialogWindow, bool>(nameof(UseCustomTitleBar));
public bool UseCustomTitleBar
{
get { return GetValue(UseCustomTitleBarProperty); }
set { SetValue(UseCustomTitleBarProperty, value); }
}
public DialogWindow() public DialogWindow()
{ {
this.HideMinMaxBtns(); KeyDown += DialogWindow_KeyDown;
this.KeyDown += DialogWindow_KeyDown; Initialized += DialogWindow_Initialized;
this.Initialized += DialogWindow_Initialized; Opened += DialogWindow_Opened;
this.Opened += DialogWindow_Opened; Closing += DialogWindow_Closing;
this.Closing += DialogWindow_Closing;
UseCustomTitleBar = Configuration.IsWindows;
} }
private bool fixedMinHeight = false;
private bool fixedMaxHeight = false;
private bool fixedHeight = false;
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
const int customTitleBarHeight = 30;
if (UseCustomTitleBar)
{
if (change.Property == MinHeightProperty && !fixedMinHeight)
{
fixedMinHeight = true;
MinHeight += customTitleBarHeight;
fixedMinHeight = false;
}
if (change.Property == MaxHeightProperty && !fixedMaxHeight)
{
fixedMaxHeight = true;
MaxHeight += customTitleBarHeight;
fixedMaxHeight = false;
}
if (change.Property == HeightProperty && !fixedHeight)
{
fixedHeight = true;
Height += customTitleBarHeight;
fixedHeight = false;
}
}
base.OnPropertyChanged(change);
}
public DialogWindow(bool saveAndRestorePosition) : this() public DialogWindow(bool saveAndRestorePosition) : this()
{ {
SaveAndRestorePosition = saveAndRestorePosition; SaveAndRestorePosition = saveAndRestorePosition;
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (!UseCustomTitleBar)
return;
var closeButton = e.NameScope.Find<Button>("DialogCloseButton");
var border = e.NameScope.Get<Border>("DialogWindowTitleBorder");
var titleBlock = e.NameScope.Get<TextBlock>("DialogWindowTitleTextBlock");
var icon = e.NameScope.Get<Avalonia.Controls.Shapes.Path>("DialogWindowTitleIcon");
closeButton.Click += CloseButton_Click;
border.PointerPressed += Border_PointerPressed;
icon.IsVisible = Icon != null;
if (MinHeight == MaxHeight && MinWidth == MaxWidth)
{
CanResize = false;
border.Margin = new Thickness(0);
icon.Margin = new Thickness(8, 5, 0, 5);
}
}
private void Border_PointerPressed(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(null).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private void CloseButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
CancelAndClose();
}
private void DialogWindow_Initialized(object sender, EventArgs e) private void DialogWindow_Initialized(object sender, EventArgs e)
{ {
this.WindowStartupLocation = WindowStartupLocation.CenterOwner; this.WindowStartupLocation = WindowStartupLocation.CenterOwner;
@@ -2,11 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350" mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="350"
Width="800" Height="450" Width="800" Height="450"
x:Class="LibationAvalonia.Dialogs.EditQuickFilters" x:Class="LibationAvalonia.Dialogs.EditQuickFilters"
Title="Audible Accounts" Title="Audible Accounts"
Icon="/Assets/libation.ico"> Icon="/Assets/libation.ico"
x:DataType="dialogs:EditQuickFilters">
<Grid RowDefinitions="*,Auto"> <Grid RowDefinitions="*,Auto">
<Grid.Styles> <Grid.Styles>
@@ -43,7 +45,14 @@
</DataTemplate> </DataTemplate>
</DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn> </DataGridTemplateColumn>
<DataGridTextColumn
Width="*"
IsReadOnly="False"
Binding="{Binding Name, Mode=TwoWay}"
Header="Name"/>
<DataGridTextColumn <DataGridTextColumn
Width="*" Width="*"
IsReadOnly="False" IsReadOnly="False"
@@ -12,8 +12,18 @@ namespace LibationAvalonia.Dialogs
public ObservableCollection<Filter> Filters { get; } = new(); public ObservableCollection<Filter> Filters { get; } = new();
public class Filter : ViewModels.ViewModelBase public class Filter : ViewModels.ViewModelBase
{ {
private string _filterString; private string _name;
public string Name
{
get => _name;
set
{
this.RaiseAndSetIfChanged(ref _name, value);
}
}
private string _filterString;
public string FilterString public string FilterString
{ {
get => _filterString; get => _filterString;
@@ -25,6 +35,9 @@ namespace LibationAvalonia.Dialogs
} }
} }
public bool IsDefault { get; private set; } = true; public bool IsDefault { get; private set; } = true;
public QuickFilters.NamedFilter AsNamedFilter() => new(FilterString, Name);
} }
public EditQuickFilters() public EditQuickFilters()
{ {
@@ -40,7 +53,7 @@ namespace LibationAvalonia.Dialogs
ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn)); ControlToFocusOnShow = this.FindControl<Button>(nameof(saveBtn));
var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f }).ToList(); var allFilters = QuickFilters.Filters.Select(f => new Filter { FilterString = f.Filter, Name = f.Name }).ToList();
allFilters.Add(new Filter()); allFilters.Add(new Filter());
foreach (var f in allFilters) foreach (var f in allFilters)
@@ -61,7 +74,7 @@ namespace LibationAvalonia.Dialogs
protected override void SaveAndClose() protected override void SaveAndClose()
{ {
QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(f => f.FilterString)); QuickFilters.ReplaceAll(Filters.Where(f => !f.IsDefault).Select(x => x.AsNamedFilter()));
base.SaveAndClose(); base.SaveAndClose();
} }
@@ -27,6 +27,6 @@
Margin="5" Margin="5"
Padding="30,3,30,3" Padding="30,3,30,3"
Content="Save" Content="Save"
Command="{Binding SaveButtonAsync}" /> Click="Save_Click" />
</Grid> </Grid>
</Window> </Window>
@@ -1,11 +1,9 @@
using Avalonia.Controls;
using LibationFileManager; using LibationFileManager;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.Dialogs namespace LibationAvalonia.Dialogs
{ {
public partial class LibationFilesDialog : Window public partial class LibationFilesDialog : DialogWindow
{ {
private class DirSelectOptions private class DirSelectOptions
{ {
@@ -18,28 +16,26 @@ namespace LibationAvalonia.Dialogs
public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile); public string Directory { get; set; } = Configuration.GetKnownDirectoryPath(Configuration.KnownDirectories.UserProfile);
} }
private DirSelectOptions dirSelectOptions;
private readonly DirSelectOptions dirSelectOptions;
public string SelectedDirectory => dirSelectOptions.Directory; public string SelectedDirectory => dirSelectOptions.Directory;
public DialogResult DialogResult { get; private set; }
public LibationFilesDialog() public LibationFilesDialog() : base(saveAndRestorePosition: false)
{ {
InitializeComponent(); InitializeComponent();
DataContext = dirSelectOptions = new(); DataContext = dirSelectOptions = new();
} }
public async Task SaveButtonAsync() public async void Save_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{ {
var libationDir = dirSelectOptions.Directory; if (!System.IO.Directory.Exists(SelectedDirectory))
if (!System.IO.Directory.Exists(libationDir))
{ {
await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + libationDir, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false); await MessageBox.Show("Not saving change to Libation Files location. This folder does not exist:\r\n" + SelectedDirectory, "Folder does not exist", MessageBoxButtons.OK, MessageBoxIcon.Error, saveAndRestorePosition: false);
return; return;
} }
DialogResult = DialogResult.OK; await SaveAndCloseAsync();
Close(DialogResult);
} }
} }
} }
@@ -25,7 +25,6 @@ namespace LibationAvalonia.Dialogs
{ {
InitializeComponent(); InitializeComponent();
this.HideMinMaxBtns();
ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton)); ControlToFocusOnShow = this.FindControl<Button>(nameof(ImportButton));
LoadAccounts(); LoadAccounts();
@@ -12,8 +12,6 @@ namespace LibationAvalonia.Dialogs
{ {
InitializeComponent(); InitializeComponent();
this.HideMinMaxBtns();
StringFields = @" StringFields = @"
Search for wizard of oz: Search for wizard of oz:
title:oz title:oz
@@ -2,8 +2,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="750" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="200"
MinWidth="900" MinHeight="750" MinWidth="900" MinHeight="200"
Width="900" Height="750" Width="900" Height="750"
x:Class="LibationAvalonia.Dialogs.SettingsDialog" x:Class="LibationAvalonia.Dialogs.SettingsDialog"
xmlns:controls="clr-namespace:LibationAvalonia.Controls" xmlns:controls="clr-namespace:LibationAvalonia.Controls"
@@ -46,39 +46,41 @@
</TabControl.Styles> </TabControl.Styles>
<TabItem> <TabItem>
<TabItem.Header> <TabItem.Header>
<TextBlock Text="Important Settings"/> <TextBlock Text="Important Settings"/>
</TabItem.Header> </TabItem.Header>
<settings:Important DataContext="{CompiledBinding ImportantSettings}" /> <ScrollViewer VerticalScrollBarVisibility="Auto">
<settings:Important DataContext="{CompiledBinding ImportantSettings}" />
</ScrollViewer>
</TabItem> </TabItem>
<TabItem> <TabItem>
<TabItem.Header> <TabItem.Header>
<TextBlock Text="Import Library"/> <TextBlock Text="Import Library"/>
</TabItem.Header> </TabItem.Header>
<settings:Import DataContext="{CompiledBinding ImportSettings}" /> <ScrollViewer VerticalScrollBarVisibility="Auto">
<settings:Import DataContext="{CompiledBinding ImportSettings}" />
</ScrollViewer>
</TabItem> </TabItem>
<TabItem> <TabItem>
<TabItem.Header> <TabItem.Header>
<TextBlock Text="Download/Decrypt"/> <TextBlock Text="Download/Decrypt"/>
</TabItem.Header> </TabItem.Header>
<settings:DownloadDecrypt DataContext="{CompiledBinding DownloadDecryptSettings}" /> <ScrollViewer VerticalScrollBarVisibility="Auto">
<settings:DownloadDecrypt DataContext="{CompiledBinding DownloadDecryptSettings}" />
</ScrollViewer>
</TabItem> </TabItem>
<TabItem> <TabItem>
<TabItem.Header> <TabItem.Header>
<TextBlock Text="Audio File Settings"/> <TextBlock Text="Audio File Settings"/>
</TabItem.Header> </TabItem.Header>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<settings:Audio DataContext="{CompiledBinding AudioSettings}" /> <settings:Audio DataContext="{CompiledBinding AudioSettings}" />
</ScrollViewer>
</TabItem> </TabItem>
</TabControl> </TabControl>
</Grid> </Grid>
@@ -2,12 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:LibationAvalonia.Dialogs"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350" mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="350"
x:Class="LibationAvalonia.Dialogs.SetupDialog" x:Class="LibationAvalonia.Dialogs.SetupDialog"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Width="500" Height="350" Width="500" Height="350"
Icon="/Assets/libation.ico" Icon="/Assets/libation.ico"
Title="Welcome to Libation"> Title="Welcome to Libation"
x:DataType="dialogs:SetupDialog">
<Grid <Grid
Margin="10" Margin="10"
@@ -58,6 +60,7 @@
SelectedIndex="0" SelectedIndex="0"
SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}"> SelectedItem="{Binding SelectedTheme, Mode=OneWayToSource}">
<ComboBox.Items> <ComboBox.Items>
<ComboBoxItem Content="System" />
<ComboBoxItem Content="Light" /> <ComboBoxItem Content="Light" />
<ComboBoxItem Content="Dark" /> <ComboBoxItem Content="Dark" />
</ComboBox.Items> </ComboBox.Items>
@@ -109,23 +109,5 @@ namespace LibationAvalonia
public int Width; public int Width;
public bool IsMaximized; public bool IsMaximized;
} }
public static void HideMinMaxBtns(this Window form)
{
if (Design.IsDesignMode || !Configuration.IsWindows || form.TryGetPlatformHandle() is not IPlatformHandle handle)
return;
var currentStyle = GetWindowLong(handle.Handle, GWL_STYLE);
SetWindowLong(handle.Handle, GWL_STYLE, currentStyle & ~WS_MAXIMIZEBOX & ~WS_MINIMIZEBOX);
}
const long WS_MINIMIZEBOX = 0x00020000L;
const long WS_MAXIMIZEBOX = 0x10000L;
const int GWL_STYLE = -16;
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "GetWindowLong")]
static extern long GetWindowLong(IntPtr hWnd, int nIndex);
[System.Runtime.InteropServices.DllImport("user32.dll")]
static extern int SetWindowLong(IntPtr hWnd, int nIndex, long dwNewLong);
} }
} }
@@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net9.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationIcon>Assets/libation.ico</ApplicationIcon> <ApplicationIcon>Assets/libation.ico</ApplicationIcon>
@@ -14,6 +14,10 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<StartupObject /> <StartupObject />
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOption Include="System.Net.DisableIPv6" Value="true" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages> <SatelliteResourceLanguages>en</SatelliteResourceLanguages>
@@ -69,14 +73,14 @@
<UpToDateCheckInput Remove="Controls\GroupBox.axaml" /> <UpToDateCheckInput Remove="Controls\GroupBox.axaml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.0" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.2.4" Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" />
<PackageReference Include="Avalonia" Version="11.0.0" /> <PackageReference Include="Avalonia" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.0" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.2.4" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.0" /> <PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.0" /> <PackageReference Include="Avalonia.Desktop" Version="11.2.4" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.0" /> <PackageReference Include="Avalonia.ReactiveUI" Version="11.2.4" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.0" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+10 -7
View File
@@ -165,8 +165,6 @@ Libation.
var dialog = new MessageBoxWindow(saveAndRestorePosition); var dialog = new MessageBoxWindow(saveAndRestorePosition);
dialog.HideMinMaxBtns();
var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton); var vm = new MessageBoxViewModel(message, caption, buttons, icon, defaultButton);
dialog.DataContext = vm; dialog.DataContext = vm;
dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString()); dialog.ControlToFocusOnShow = dialog.FindControl<Control>(defaultButton.ToString());
@@ -177,9 +175,12 @@ Libation.
tbx.MinWidth = vm.TextBlockMinWidth; tbx.MinWidth = vm.TextBlockMinWidth;
tbx.Text = message; tbx.Text = message;
var thisScreen = owner.Screens.ScreenFromVisual(owner);
var thisScreen = owner.Screens?.ScreenFromVisual(owner);
var maxSize = new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55); var maxSize
= thisScreen is null ? owner.ClientSize
: new Size(0.20 * thisScreen.Bounds.Width, 0.9 * thisScreen.Bounds.Height - 55);
var desiredMax = new Size(maxSize.Width, maxSize.Height); var desiredMax = new Size(maxSize.Width, maxSize.Height);
@@ -187,11 +188,13 @@ Libation.
tbx.Height = tbx.DesiredSize.Height; tbx.Height = tbx.DesiredSize.Height;
tbx.Width = tbx.DesiredSize.Width; tbx.Width = tbx.DesiredSize.Width;
dialog.MinHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
var absoluteHeight = vm.FormHeightFromTboxHeight((int)tbx.DesiredSize.Height);
dialog.MinHeight = absoluteHeight;
dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width); dialog.MinWidth = vm.FormWidthFromTboxWidth((int)tbx.DesiredSize.Width);
dialog.MaxHeight = dialog.MinHeight; dialog.MaxHeight = absoluteHeight;
dialog.MaxWidth = dialog.MinWidth; dialog.MaxWidth = dialog.MinWidth;
dialog.Height = dialog.MinHeight; dialog.Height = absoluteHeight;
dialog.Width = dialog.MinWidth; dialog.Width = dialog.MinWidth;
return dialog; return dialog;
} }
+1 -8
View File
@@ -46,11 +46,6 @@ namespace LibationAvalonia
try try
{ {
var config = LibationScaffolding.RunPreConfigMigrations(); var config = LibationScaffolding.RunPreConfigMigrations();
//Start as much work in parallel as possible.
var classicLifetimeTask = Task.Run(() => new ClassicDesktopStyleApplicationLifetime());
var appBuilderTask = Task.Run(BuildAvaloniaApp);
if (config.LibationSettingsAreValid) if (config.LibationSettingsAreValid)
{ {
// most migrations go in here // most migrations go in here
@@ -62,9 +57,7 @@ namespace LibationAvalonia
App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); App.LibraryTask = Task.Run(() => DbContexts.GetLibrary_Flat_NoTracking(includeParents: true));
} }
appBuilderTask.GetAwaiter().GetResult().SetupWithLifetime(classicLifetimeTask.GetAwaiter().GetResult()); BuildAvaloniaApp().StartWithClassicDesktopLifetime(null);
classicLifetimeTask.Result.Start(null);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir> <PublishDir>..\bin\Publish\Linux-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>linux-x64</RuntimeIdentifier> <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir> <PublishDir>..\bin\Publish\MacOS-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>osx-x64</RuntimeIdentifier> <RuntimeIdentifier>osx-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
@@ -8,7 +8,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121.
<Platform>Any CPU</Platform> <Platform>Any CPU</Platform>
<PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir> <PublishDir>..\bin\Publish\Windows-chardonnay</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol> <PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net7.0-windows</TargetFramework> <TargetFramework>net9.0-windows</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained> <SelfContained>true</SelfContained>
<PublishSingleFile>false</PublishSingleFile> <PublishSingleFile>false</PublishSingleFile>
@@ -4,7 +4,6 @@ using DataLayer;
using LibationFileManager; using LibationFileManager;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
@@ -44,16 +43,15 @@ namespace LibationAvalonia.ViewModels
private void Configure_BackupCounts() private void Configure_BackupCounts()
{ {
MainWindow.LibraryLoaded += (_, e) => setBackupCounts(e.Where(l => !l.Book.IsEpisodeParent())); //Pass null to the setup count to get the whole library.
LibraryCommands.LibrarySizeChanged += (_,_) => setBackupCounts(); LibraryCommands.BookUserDefinedItemCommitted += async (_, _)
LibraryCommands.BookUserDefinedItemCommitted += (_, _) => setBackupCounts(); => await SetBackupCountsAsync(null);
} }
private async void setBackupCounts(IEnumerable<LibraryBook> libraryBooks = null) public async Task SetBackupCountsAsync(IEnumerable<LibraryBook> libraryBooks)
{ {
if (updateCountsTask?.IsCompleted ?? true) if (updateCountsTask?.IsCompleted ?? true)
{ {
libraryBooks ??= DbContexts.GetLibrary_Flat_NoTracking();
updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks)); updateCountsTask = Task.Run(() => LibraryCommands.GetCounts(libraryBooks));
var stats = await updateCountsTask; var stats = await updateCountsTask;
await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats); await Dispatcher.UIThread.InvokeAsync(() => LibraryStats = stats);
@@ -13,12 +13,12 @@ namespace LibationAvalonia.ViewModels
{ {
partial class MainVM partial class MainVM
{ {
private string lastGoodFilter = ""; private QuickFilters.NamedFilter lastGoodFilter = new(string.Empty, null);
private string _filterString; private QuickFilters.NamedFilter _selectedNamedFilter = new(string.Empty, null);
private bool _firstFilterIsDefault = true; private bool _firstFilterIsDefault = true;
/// <summary> Library filterting query </summary> /// <summary> Library filterting query </summary>
public string FilterString { get => _filterString; set => this.RaiseAndSetIfChanged(ref _filterString, value); } public QuickFilters.NamedFilter SelectedNamedFilter { get => _selectedNamedFilter; set => this.RaiseAndSetIfChanged(ref _selectedNamedFilter, value); }
public AvaloniaList<Control> QuickFilterMenuItems { get; } = new(); public AvaloniaList<Control> QuickFilterMenuItems { get; } = new();
/// <summary> Indicates if the first quick filter is the default filter </summary> /// <summary> Indicates if the first quick filter is the default filter </summary>
public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); } public bool FirstFilterIsDefault { get => _firstFilterIsDefault; set => QuickFilters.UseDefault = this.RaiseAndSetIfChanged(ref _firstFilterIsDefault, value); }
@@ -50,19 +50,19 @@ namespace LibationAvalonia.ViewModels
QuickFilterMenuItems.Add(new Separator()); QuickFilterMenuItems.Add(new Separator());
} }
public void AddQuickFilterBtn() => QuickFilters.Add(FilterString); public void AddQuickFilterBtn() => QuickFilters.Add(SelectedNamedFilter);
public async Task FilterBtn() => await PerformFilter(FilterString); public async Task FilterBtn() => await PerformFilter(SelectedNamedFilter);
public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow); public async Task FilterHelpBtn() => await new LibationAvalonia.Dialogs.SearchSyntaxDialog().ShowDialog(MainWindow);
public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault; public void ToggleFirstFilterIsDefault() => FirstFilterIsDefault = !FirstFilterIsDefault;
public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow); public async Task EditQuickFiltersAsync() => await new LibationAvalonia.Dialogs.EditQuickFilters().ShowDialog(MainWindow);
public async Task PerformFilter(string filterString) public async Task PerformFilter(QuickFilters.NamedFilter namedFilter)
{ {
FilterString = filterString; SelectedNamedFilter = namedFilter;
try try
{ {
await ProductsDisplay.Filter(filterString); await ProductsDisplay.Filter(namedFilter.Filter);
lastGoodFilter = filterString; lastGoodFilter = namedFilter;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -99,8 +99,8 @@ namespace LibationAvalonia.ViewModels
{ {
var command = ReactiveCommand.Create(async () => await PerformFilter(filter)); var command = ReactiveCommand.Create(async () => await PerformFilter(filter));
var menuItem = new MenuItem { Header = $"{++index}: {filter}", Command = command }; var menuItem = new MenuItem { Header = $"{++index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {filter}", Command = command }; var nativeMenuItem = new NativeMenuItem { Header = $"{index}: {(string.IsNullOrWhiteSpace(filter.Name) ? filter.Filter : filter.Name)}", Command = command };
if (Configuration.IsMacOs && index <= 10) if (Configuration.IsMacOs && index <= 10)
{ {
@@ -10,8 +10,8 @@ namespace LibationAvalonia.ViewModels
{ {
partial class MainVM partial class MainVM
{ {
private int _visibleNotLiberated = 1; private int _visibleNotLiberated = 0;
private int _visibleCount = 1; private int _visibleCount = 0;
/// <summary> The Bottom-right visible book count status text </summary> /// <summary> The Bottom-right visible book count status text </summary>
public string VisibleCountText => $"Visible: {_visibleCount}"; public string VisibleCountText => $"Visible: {_visibleCount}";
+11 -1
View File
@@ -1,7 +1,10 @@
using ApplicationServices; using ApplicationServices;
using DataLayer;
using LibationAvalonia.Views; using LibationAvalonia.Views;
using LibationFileManager; using LibationFileManager;
using ReactiveUI; using ReactiveUI;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace LibationAvalonia.ViewModels namespace LibationAvalonia.ViewModels
{ {
@@ -20,7 +23,7 @@ namespace LibationAvalonia.ViewModels
MainWindow = mainWindow; MainWindow = mainWindow;
ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation"; ProductsDisplay.RemovableCountChanged += (_, removeCount) => RemoveBooksButtonText = removeCount == 1 ? "Remove 1 Book from Libation" : $"Remove {removeCount} Books from Libation";
LibraryCommands.LibrarySizeChanged += async (_, _) => await ProductsDisplay.UpdateGridAsync(DbContexts.GetLibrary_Flat_NoTracking(includeParents: true)); LibraryCommands.LibrarySizeChanged += LibraryCommands_LibrarySizeChanged;
Configure_NonUI(); Configure_NonUI();
Configure_BackupCounts(); Configure_BackupCounts();
@@ -34,6 +37,13 @@ namespace LibationAvalonia.ViewModels
Configure_VisibleBooks(); Configure_VisibleBooks();
} }
private async void LibraryCommands_LibrarySizeChanged(object sender, List<LibraryBook> fullLibrary)
{
await Task.WhenAll(
SetBackupCountsAsync(fullLibrary),
ProductsDisplay.UpdateGridAsync(fullLibrary));
}
private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}"; private static string menufyText(string header) => Configuration.IsMacOs ? header : $"_{header}";
} }
} }
@@ -265,7 +265,7 @@ namespace LibationAvalonia.ViewModels
private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e) private byte[] AudioDecodable_RequestCoverArt(object sender, EventArgs e)
{ {
var quality var quality
= Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High = Configuration.Instance.FileDownloadQuality == Configuration.DownloadQuality.High && LibraryBook.Book.PictureLarge is not null
? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native) ? new PictureDefinition(LibraryBook.Book.PictureLarge, PictureSize.Native)
: new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500); : new PictureDefinition(LibraryBook.Book.PictureId, PictureSize._500x500);
@@ -118,7 +118,15 @@ namespace LibationAvalonia.ViewModels
#region Add Books to Queue #region Add Books to Queue
private bool isBookInQueue(LibraryBook libraryBook) private bool isBookInQueue(LibraryBook libraryBook)
=> Queue.Any(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId); {
var entry = Queue.FirstOrDefault(b => b?.LibraryBook?.Book?.AudibleProductId == libraryBook.Book.AudibleProductId);
if (entry == null)
return false;
else if (entry.Status is ProcessBookStatus.Cancelled or ProcessBookStatus.Failed)
return !Queue.RemoveCompleted(entry);
else
return true;
}
public void AddDownloadPdf(LibraryBook libraryBook) public void AddDownloadPdf(LibraryBook libraryBook)
=> AddDownloadPdf(new List<LibraryBook>() { libraryBook }); => AddDownloadPdf(new List<LibraryBook>() { libraryBook });
Loaded 100 of 206 files, more files were not shown because too many files have changed in this diff. Show more