Compare commits

...

34 Commits

Author SHA1 Message Date
Robert McRackan
f72551fa9a incr ver 2023-01-19 08:06:18 -05:00
rmcrackan
e3b237b75f Merge pull request #454 from Mbucari/master
Fixed #453
2023-01-18 22:23:55 -05:00
Mbucari
b1ddf18f73 Merge branch 'rmcrackan:master' into master 2023-01-18 15:47:18 -07:00
Michael Bucari-Tovo
13f522abb8 Fix file extension detection error (#453) 2023-01-18 15:47:05 -07:00
Robert McRackan
3c3d956bf3 remove linux and mac from beta 2023-01-16 13:16:49 -05:00
Robert McRackan
8160547c11 incr ver 2023-01-16 13:06:19 -05:00
rmcrackan
ef71d36dee Merge pull request #451 from Mbucari/patch-3
Updated for new deb package builds
2023-01-16 12:59:23 -05:00
Mbucari
b0d8434455 Updated for new deb package builds 2023-01-16 10:41:21 -07:00
rmcrackan
9be0d58461 Merge pull request #450 from Mbucari/master
Update workflows and AAXClean
2023-01-16 10:38:18 -05:00
Michael Bucari-Tovo
1addcc8211 Update AAXClean and add better error handling 2023-01-15 21:42:03 -07:00
Mbucari
38c75dc8c5 Update workflows 2023-01-12 15:47:01 -07:00
rmcrackan
89c3ea8311 Merge pull request #444 from Mbucari/master
Build and attach deb package
2023-01-12 08:45:14 -05:00
Michael Bucari-Tovo
18ff799fb1 Update build scripts 2023-01-11 21:14:26 -07:00
Mbucari
67b6aaed99 Fix #447 2023-01-11 16:08:01 -07:00
Mbucari
08bb463560 Invoke observables directly instead of through event handler 2023-01-11 14:38:12 -07:00
Mbucari
97767dcabb GLOB pattern 2023-01-11 14:37:12 -07:00
Mbucari
fb18940a5c Use DataGridMyRatingColumn for both user and product ratings. 2023-01-11 14:36:43 -07:00
Mbucari
b823f5fa00 Import average rating 2023-01-11 14:35:14 -07:00
Mbucari
d64fb081a0 Build and attach deb package 2023-01-11 14:15:07 -07:00
Robert McRackan
09118b1ddf nvm. I guess this has been default : true for a long time. Reverting 2023-01-10 10:13:47 -05:00
Robert McRackan
de20590fd5 do not enable AutoScan by default 2023-01-10 10:04:53 -05:00
rmcrackan
1050ffdb24 Merge pull request #443 from Mbucari/master
Chardonnay Bugfix + Moved Default Settings into Configuration
2023-01-10 09:54:49 -05:00
Michael Bucari-Tovo
2e49c7f697 Commit edits before refresh 2023-01-09 18:57:16 -07:00
Mbucari
708cdcc24c Merge branch 'master' of https://github.com/Mbucari/Libation 2023-01-09 16:30:59 -07:00
Mbucari
c89eafd568 Fix rating edits updating search results. 2023-01-09 16:27:19 -07:00
Michael Bucari-Tovo
10de241d53 Cache default values 2023-01-09 16:11:30 -07:00
Mbucari
e58952035f Spaces 2023-01-09 16:06:37 -07:00
Mbucari
50a8c7508a Cache default values 2023-01-09 16:05:55 -07:00
Michael Bucari-Tovo
2b243a6934 Remove testing code 2023-01-09 15:28:36 -07:00
Michael Bucari-Tovo
ece93cb4d7 Finish migrating default Configuration values into Configuration 2023-01-09 15:26:06 -07:00
rmcrackan
5b3ca0ed32 Merge pull request #442 from wtanksleyjr/master
Add a version parameter to DEB creation, do sanity checks, use it.
2023-01-09 16:36:48 -05:00
William Tanksley
d023a943c1 Add a version parameter to DEB creation, do sanity checks, use it. 2023-01-09 13:12:12 -08:00
Michael Bucari-Tovo
4e80af5c53 Bind MyRatingCellEditor background color to the cell's background 2023-01-09 14:06:06 -07:00
Michael Bucari-Tovo
eee785377f Add default values to Configuration 2023-01-09 14:05:33 -07:00
54 changed files with 488 additions and 505 deletions

39
.github/workflows/build-deb.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# build-deb.yml
# Reusable workflow that builds the Linux Debian package.
---
name: build_deb
on:
workflow_call:
inputs:
version:
type: string
description: 'Version number'
required: true
env:
FILE_NAME: "Libation.${{ inputs.version }}-linux-chardonnay"
jobs:
build_deb:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Download Artifact
uses: actions/download-artifact@master
with:
name: "${{ env.FILE_NAME }}.tar.gz"
- name: Build .deb
id: deb
run: |
chmod +x ./.github/workflows/scripts/targz2deb.sh
./.github/workflows/scripts/targz2deb.sh "${{ env.FILE_NAME }}.tar.gz" ${{ inputs.version }}
- name: Publish .deb
uses: actions/upload-artifact@v3
with:
name: ${{ env.FILE_NAME }}.deb
path: ${{ env.FILE_NAME }}.deb
if-no-files-found: error

View File

@@ -15,6 +15,10 @@ on:
description: 'Skip running unit tests'
required: false
default: true
outputs:
version:
description: "The Libation version number"
value: ${{ jobs.build.outputs.version }}
env:
DOTNET_CONFIGURATION: 'Release'
@@ -23,6 +27,8 @@ env:
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
strategy:
matrix:
os: [Linux, MacOS]
@@ -66,6 +72,8 @@ jobs:
id: zip
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}
run: |
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll" "ZipExtractor.exe")
for n in "${delfiles[@]}"; do rm "$n"; done
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')"
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}"
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"

View File

@@ -15,6 +15,10 @@ on:
description: 'Skip running unit tests'
required: false
default: true
outputs:
version:
description: "The Libation version number"
value: ${{ jobs.build.outputs.version }}
env:
DOTNET_CONFIGURATION: 'Release'
@@ -23,6 +27,8 @@ env:
jobs:
build:
runs-on: windows-latest
outputs:
version: ${{ steps.get_version.outputs.version }}
strategy:
matrix:
os: [Windows]
@@ -48,8 +54,7 @@ jobs:
if ("${{ inputs.version_override }}".length -gt 0) {
$version = "${{ inputs.version_override }}"
} else {
[xml]$appScaffolding = Get-Content -Path ./Source/AppScaffolding/AppScaffolding.csproj
$version = $appScaffolding.Project.PropertyGroup.Version
$version = (Select-Xml -Path "./Source/AppScaffolding/AppScaffolding.csproj" -XPath "/Project/PropertyGroup/Version").Node.InnerXML.Trim()
}
"version=$version" >> $env:GITHUB_OUTPUT
@@ -69,10 +74,13 @@ jobs:
- name: Zip artifact
id: zip
working-directory: ./Source/bin/Publish
run: |
run: |
$dir = "${{ matrix.os }}-${{ matrix.release_name }}\"
$delfiles = @("libmp3lame.so", "ffmpegaac.so", "glass-with-glow_256.svg", "Libation.desktop")
foreach ($file in $delfiles){ if (test-path $dir$file){ Remove-Item $dir$file } }
$artifact="${{ matrix.prefix }}Libation.${{ steps.get_version.outputs.version }}-" + "${{ matrix.os }}".ToLower() + "-${{ matrix.release_name }}"
"artifact=$artifact" >> $env:GITHUB_OUTPUT
Compress-Archive -Path "${{ matrix.os }}-${{ matrix.release_name }}\*" -DestinationPath "$artifact.zip"
Compress-Archive -Path "${dir}*" -DestinationPath "$artifact.zip"
- name: Publish artifact
uses: actions/upload-artifact@v3
@@ -80,3 +88,4 @@ jobs:
name: ${{ steps.zip.outputs.artifact }}.zip
path: ./Source/bin/Publish/${{ steps.zip.outputs.artifact }}.zip
if-no-files-found: error

View File

@@ -14,7 +14,12 @@ on:
type: boolean
description: 'Skip running unit tests'
required: false
default: true
default: true
build_deb:
type: boolean
description: 'Build Debian package'
required: false
default: false
jobs:
windows:
@@ -28,3 +33,11 @@ jobs:
with:
version_override: ${{ inputs.version_override }}
run_unit_tests: ${{ inputs.run_unit_tests }}
linux_deb:
needs: [linux]
if: inputs.build_deb
uses: ./.github/workflows/build-deb.yml
with:
version: ${{ needs.linux.outputs.version }}

View File

@@ -33,6 +33,7 @@ jobs:
with:
version_override: ${{ needs.prerelease.outputs.version }}
run_unit_tests: false
build_deb: true
release:
needs: [prerelease,build]
@@ -50,7 +51,7 @@ jobs:
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
with:
tag_name: '${{ github.ref }}'
release_name: 'Libation ${{ steps.version.outputs.version }}'
release_name: 'Libation ${{ needs.prerelease.outputs.version }}'
body: <Put a body here>
draft: true
prerelease: false

View File

@@ -1,6 +1,7 @@
#!/bin/bash
FILE=$1
FILE=$1; shift
VERSION=$1; shift
if [ -z "$FILE" ]
then
@@ -14,6 +15,20 @@ then
exit
fi
if [ -z "$VERSION" ]
then
echo "This script must be called with the Libation version number as an argument."
exit
fi
contains() { case "$1" in *"$2"*) true ;; *) false ;; esac }
if ! contains "$FILE" "$VERSION"
then
echo "This script must be called with a Libation version number that is present in the filename passed."
exit
fi
# remove trailing ".tar.gz"
FOLDER_MAIN=${FILE::-7}
echo "Working dir: $FOLDER_MAIN"
@@ -90,6 +105,9 @@ ln -s /usr/lib/libation/Libation /usr/bin/libation
ln -s /usr/lib/libation/Hangover /usr/bin/hangover
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
# Increase the maximum number of inotify instances
echo fs.inotify.max_user_instances=524288 | tee -a /etc/sysctl.conf && sysctl -p
# 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
@@ -97,11 +115,10 @@ chmod 666 /usr/lib/libation/appsettings.json
echo "Creating control file..."
echo "Package: Libation
Version: 8.7.0
Version: $VERSION
Architecture: all
Essential: no
Priority: optional
Depends: ffmpeg
Maintainer: github.com/rmcrackan
Description: liberate your audiobooks
" >> "$FOLDER_DEBIAN/control"
@@ -116,3 +133,4 @@ dpkg-deb --build $FOLDER_MAIN
rm -r "$FOLDER_MAIN"
echo "Done!"

View File

@@ -26,8 +26,8 @@
Extract the zip file to a folder and then run `Libation.exe` from inside of that folder. Do not put it in Program Files. The inability to edit files from there causes problems with configuration and updating.
* [Ubuntu Linux (beta)](InstallOnLinux.md)
* [MacOS (beta)](InstallOnMac.md)
* [Ubuntu Linux](InstallOnLinux.md)
* [MacOS](InstallOnMac.md)
### Create Accounts

View File

@@ -4,97 +4,19 @@
...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
# Run Libation on Ubuntu (Beta)
This walkthrough should get you up and running with Libation on your Ubuntu machine.
New Libation releases are automatically packed into a debian package and are available from the Libation repository's releases page.
Some limitations of the linux release are:
- Cannot customize how illegial filename characters are replaced.
- The Auto-update function is unavailable
Run this command in your terminal to dowbnload and install Libation, replacing the url with the Latest Libation .deb package url:
## Dependencies
### FFMpeg (Optional)
If you want to convert your audiobooks to mp3, install FFMpeg using the following command:
```console
sudo apt-get install -y ffmpeg
```
## Install Libation
Download the most recent linux-64 binaries zip file and save it as `libation-linux-bin.zip`. Save the 'install-libation.sh' bash script to a file. From the terminal make the script file executable:
<details>
<summary>install-libation.sh</summary>
```BASH
#!/bin/bash
FILE=$1
if [ -z "$FILE" ]
then echo "This script must be called with a the Libation Linux bin zip file as an argument."
exit
fi
if [[ "$EUID" -ne 0 ]]
then echo "Please run as root"
exit
fi
if [ ! -f "$FILE" ]
then echo "The file \"$FILE\" does not exist."
exit
fi
echo "Extracting $FILE"
FOLDER="$(dirname "$FILE")/libation_src"
echo "$FOLDER"
sudo -u $SUDO_USER unzip -q -o ${FILE} -d ${FOLDER}
if [ $? -ne 0 ]
then echo "Error unzipping ${FILE}"
exit
fi
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Libation
sudo -u $SUDO_USER chmod +700 ${FOLDER}/Hangover
sudo -u $SUDO_USER chmod +700 ${FOLDER}/LibationCli
#Remove previous installation program files and sym link
rm /usr/bin/Libation
rm /usr/bin/Hangover
rm /usr/bin/LibationCli
rm /usr/bin/libationcli
rm /usr/lib/libation -r
#Copy install files, icon and desktop file
cp ${FOLDER}/glass-with-glow_256.svg /usr/share/icons/hicolor/scalable/apps/libation.svg
cp ${FOLDER}/Libation.desktop /usr/share/applications/Libation.desktop
mv ${FOLDER}/ /usr/lib/libation
chmod +666 /usr/share/icons/hicolor/scalable/apps/libation.svg
gtk-update-icon-cache -f /usr/share/icons/hicolor/
ln -s /usr/lib/libation/Libation /usr/bin/Libation
ln -s /usr/lib/libation/Hangover /usr/bin/Hangover
ln -s /usr/lib/libation/LibationCli /usr/bin/LibationCli
ln -s /usr/lib/libation/LibationCli /usr/bin/libationcli
echo "Done!"
```
</details>
```console
chmod +700 install-libation.sh
```
Then run the script with the libation binaries zipfile as an argument.
```console
sudo ./install-libation.sh libation-linux-bin.zip
```Console
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
```
You should now see Libation among your applications.
Additionally, you may launch Libation, LibationCli, and Hangover (the Libation recovery app) via the command line using 'libation, libationcli', and 'hangover' aliases respectively.
Report bugs to https://github.com/rmcrackan/Libation/issues

View File

@@ -5,7 +5,7 @@
# Run Libation on MacOS (Beta)
# Run Libation on MacOS
This walkthrough should get you up and running with Libation on your Mac.
## Install Libation

View File

@@ -13,7 +13,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AAXClean.Codecs" Version="0.2.15" />
<PackageReference Include="AAXClean.Codecs" Version="0.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -32,7 +32,7 @@ namespace AaxDecrypter
AaxFile.AppleTags.Album = AaxFile.AppleTags.Album?.Replace(" (Unabridged)", "");
}
if (DownloadOptions.FixupFile)
if (DownloadOptions.FixupFile && !string.IsNullOrWhiteSpace(AaxFile.AppleTags.Narrator))
AaxFile.AppleTags.AppleListBox.EditOrAddTag("TCOM", AaxFile.AppleTags.Narrator);
//Finishing configuring lame encoder.

View File

@@ -13,6 +13,7 @@ namespace AaxDecrypter
{
private static TimeSpan minChapterLength { get; } = TimeSpan.FromSeconds(3);
private List<string> multiPartFilePaths { get; } = new List<string>();
private FileStream workingFileStream;
public AaxcDownloadMultiConverter(string outFileName, string cacheDirectory, IDownloadOptions dlOptions)
: base(outFileName, cacheDirectory, dlOptions) { }
@@ -130,18 +131,31 @@ That naming may not be desirable for everyone, but it's an easy change to instea
// reset, just in case
multiPartFilePaths.Clear();
ConversionResult result;
try
{
ConversionResult result;
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
result = await ConvertToMultiMp4a(splitChapters);
else
result = await ConvertToMultiMp3(splitChapters);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
if (DownloadOptions.OutputFormat == OutputFormat.M4b)
result = await ConvertToMultiMp4a(splitChapters);
else
result = await ConvertToMultiMp3(splitChapters);
Step_DownloadAudiobook_End(zeroProgress);
return result == ConversionResult.NoErrorsDetected;
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "AAXClean Error");
workingFileStream?.Close();
FileUtility.SaferDelete(workingFileStream.Name);
return false;
}
finally
{
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
return result == ConversionResult.NoErrorsDetected;
Step_DownloadAudiobook_End(zeroProgress);
}
}
private Task<ConversionResult> ConvertToMultiMp4a(ChapterInfo splitChapters)
@@ -189,15 +203,16 @@ That naming may not be desirable for everyone, but it's an easy change to instea
private FileStream createOutputFileStream(MultiConvertFileProperties multiConvertFileProperties)
{
var fileName = DownloadOptions.GetMultipartFileName(multiConvertFileProperties);
fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters);
var extension = Path.GetExtension(fileName);
fileName = FileUtility.GetValidFilename(fileName, DownloadOptions.ReplacementCharacters, extension);
multiPartFilePaths.Add(fileName);
FileUtility.SaferDelete(fileName);
var file = File.Open(fileName, FileMode.OpenOrCreate);
workingFileStream = File.Open(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
OnFileCreated(fileName);
return file;
return workingFileStream;
}
}
}

View File

@@ -92,17 +92,28 @@ namespace AaxDecrypter
AaxFile.ConversionProgressUpdate += AaxFile_ConversionProgressUpdate;
ConversionResult decryptionResult = await decryptAsync(outputFile);
try
{
ConversionResult decryptionResult = await decryptAsync(outputFile);
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
return success;
}
catch(Exception ex)
{
Serilog.Log.Error(ex, "AAXClean Error");
FileUtility.SaferDelete(OutputFileName);
return false;
}
finally
{
outputFile.Close();
AaxFile.ConversionProgressUpdate -= AaxFile_ConversionProgressUpdate;
Step_DownloadAudiobook_End(zeroProgress);
var success = decryptionResult == ConversionResult.NoErrorsDetected && !IsCanceled;
if (success)
base.OnFileCreated(OutputFileName);
return success;
Step_DownloadAudiobook_End(zeroProgress);
}
}
private Task<ConversionResult> decryptAsync(Stream outputFile)

View File

@@ -93,7 +93,7 @@ namespace AaxDecrypter
try
{
var path = Path.ChangeExtension(OutputFileName, ".cue");
path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters);
path = FileUtility.GetValidFilename(path, DownloadOptions.ReplacementCharacters, ".cue");
File.WriteAllText(path, Cue.CreateContents(Path.GetFileName(OutputFileName), DownloadOptions.ChapterInfo));
OnFileCreated(path);
}

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Version>8.8.2.1</Version>
<Version>9.0.1.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="4.0.3" />

View File

@@ -77,7 +77,6 @@ namespace AppScaffolding
public static void RunPostConfigMigrations(Configuration config)
{
AudibleApiStorage.EnsureAccountsSettingsFileExists();
PopulateMissingConfigValues(config);
//
// migrations go below here
@@ -86,110 +85,6 @@ namespace AppScaffolding
Migrations.migrate_to_v6_6_9(config);
}
public static void PopulateMissingConfigValues(Configuration config)
{
config.InProgress ??= Configuration.WinTemp;
if (!config.Exists(nameof(config.UseCoverAsFolderIcon)))
config.UseCoverAsFolderIcon = false;
if (!config.Exists(nameof(config.BetaOptIn)))
config.BetaOptIn = false;
if (!config.Exists(nameof(config.AllowLibationFixup)))
config.AllowLibationFixup = true;
if (!config.Exists(nameof(config.CreateCueSheet)))
config.CreateCueSheet = true;
if (!config.Exists(nameof(config.RetainAaxFile)))
config.RetainAaxFile = false;
if (!config.Exists(nameof(config.SplitFilesByChapter)))
config.SplitFilesByChapter = false;
if (!config.Exists(nameof(config.StripUnabridged)))
config.StripUnabridged = false;
if (!config.Exists(nameof(config.StripAudibleBrandAudio)))
config.StripAudibleBrandAudio = false;
if (!config.Exists(nameof(config.DecryptToLossy)))
config.DecryptToLossy = false;
if (!config.Exists(nameof(config.LameTargetBitrate)))
config.LameTargetBitrate = false;
if (!config.Exists(nameof(config.LameDownsampleMono)))
config.LameDownsampleMono = true;
if (!config.Exists(nameof(config.LameBitrate)))
config.LameBitrate = 64;
if (!config.Exists(nameof(config.LameConstantBitrate)))
config.LameConstantBitrate = false;
if (!config.Exists(nameof(config.LameMatchSourceBR)))
config.LameMatchSourceBR = true;
if (!config.Exists(nameof(config.LameVBRQuality)))
config.LameVBRQuality = 2;
if (!config.Exists(nameof(config.BadBook)))
config.BadBook = Configuration.BadBookAction.Ask;
if (!config.Exists(nameof(config.ShowImportedStats)))
config.ShowImportedStats = true;
if (!config.Exists(nameof(config.ImportEpisodes)))
config.ImportEpisodes = true;
if (!config.Exists(nameof(config.DownloadEpisodes)))
config.DownloadEpisodes = true;
if (!config.Exists(nameof(config.ReplacementCharacters)))
config.ReplacementCharacters = FileManager.ReplacementCharacters.Default;
if (!config.Exists(nameof(config.FolderTemplate)))
config.FolderTemplate = Templates.Folder.DefaultTemplate;
if (!config.Exists(nameof(config.FileTemplate)))
config.FileTemplate = Templates.File.DefaultTemplate;
if (!config.Exists(nameof(config.ChapterFileTemplate)))
config.ChapterFileTemplate = Templates.ChapterFile.DefaultTemplate;
if (!config.Exists(nameof(config.ChapterTitleTemplate)))
config.ChapterTitleTemplate = Templates.ChapterTitle.DefaultTemplate;
if (!config.Exists(nameof(config.AutoScan)))
config.AutoScan = true;
if (!config.Exists(nameof(config.GridColumnsVisibilities)))
config.GridColumnsVisibilities = new Dictionary<string, bool>();
if (!config.Exists(nameof(config.GridColumnsDisplayIndices)))
config.GridColumnsDisplayIndices = new Dictionary<string, int>();
if (!config.Exists(nameof(config.GridColumnsWidths)))
config.GridColumnsWidths = new Dictionary<string, int>();
if (!config.Exists(nameof(config.DownloadCoverArt)))
config.DownloadCoverArt = true;
if (!config.Exists(nameof(config.DownloadClipsBookmarks)))
config.DownloadClipsBookmarks = false;
if (!config.Exists(nameof(config.ClipsBookmarksFileFormat)))
config.ClipsBookmarksFileFormat = Configuration.ClipBookmarkFormat.CSV;
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
config.AutoDownloadEpisodes = false;
if (!config.Exists(nameof(config.DownloadSpeedLimit)))
config.DownloadSpeedLimit = 0;
}
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
public static void RunPostMigrationScaffolding(Configuration config)
{

View File

@@ -93,10 +93,12 @@ namespace DataLayer
var starString = new string(STAR, fullStars);
if (score - fullStars >= 0.25f)
starString += HALF;
if (score - fullStars >= 0.75f)
starString += STAR;
else if (score - fullStars >= 0.25f)
starString += HALF;
return starString;
return starString;
}
}
}

View File

@@ -174,7 +174,10 @@ namespace DtoImporterService
if (item.PictureLarge is not null)
book.PictureLarge = item.PictureLarge;
book.UpdateProductRating(item.Product_OverallStars, item.Product_PerformanceStars, item.Product_StoryStars);
book.UpdateProductRating(
(float)(item.Rating?.OverallDistribution?.AverageRating ?? 0),
(float)(item.Rating?.PerformanceDistribution?.AverageRating ?? 0),
(float)(item.Rating?.StoryDistribution?.AverageRating ?? 0));
// important to update user-specific info. this will have changed if user has rated/reviewed the book since last library import
book.UserDefinedItem.UpdateRating(item.MyUserRating_Overall, item.MyUserRating_Performance, item.MyUserRating_Story);

View File

@@ -64,30 +64,40 @@ namespace FileLiberator
config.LameMatchSourceBR);
using var mp3File = File.OpenWrite(Path.GetTempFileName());
var result = await m4bBook.ConvertToMp3Async(mp3File, lameConfig);
m4bBook.InputStream.Close();
mp3File.Close();
if (result == ConversionResult.Failed)
try
{
FileUtility.SaferDelete(mp3File.Name);
var result = await m4bBook.ConvertToMp3Async(mp3File, lameConfig);
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters);
OnFileCreated(libraryBook, realMp3Path);
if (result == ConversionResult.Failed)
{
FileUtility.SaferDelete(mp3File.Name);
}
else if (result == ConversionResult.Cancelled)
{
FileUtility.SaferDelete(mp3File.Name);
return new StatusHandler { "Cancelled" };
}
}
catch (Exception ex)
{
Serilog.Log.Error(ex, "AAXClean error");
return new StatusHandler { "Conversion failed" };
}
else if (result == ConversionResult.Cancelled)
finally
{
FileUtility.SaferDelete(mp3File.Name);
return new StatusHandler { "Cancelled" };
m4bBook.InputStream.Close();
mp3File.Close();
}
var realMp3Path = FileUtility.SaferMoveToValidPath(mp3File.Name, proposedMp3Path, Configuration.Instance.ReplacementCharacters);
OnFileCreated(libraryBook, realMp3Path);
}
return new StatusHandler();
}
finally
{
OnCompleted(libraryBook);
}
return new StatusHandler();
}
private void M4bBook_ConversionProgressUpdate(object sender, ConversionProgressEventArgs e)

View File

@@ -13,7 +13,7 @@ namespace FileManager
public FileNamingTemplate(string template) : base(template) { }
/// <summary>Generate a valid path for this file or directory</summary>
public LongPath GetFilePath(ReplacementCharacters replacements, bool returnFirstExisting = false)
public LongPath GetFilePath(ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
{
string fileName =
Template.EndsWith(Path.DirectorySeparatorChar) || Template.EndsWith(Path.AltDirectorySeparatorChar) ?
@@ -44,7 +44,6 @@ namespace FileManager
var fileNamePart = pathParts[^1];
pathParts.Remove(fileNamePart);
var fileExtension = Path.GetExtension(fileNamePart);
fileNamePart = fileNamePart[..^fileExtension.Length];
LongPath directory = Path.Join(pathParts.Select(p => replaceFileName(p, paramReplacements, LongPath.MaxFilenameLength)).ToArray());
@@ -56,6 +55,7 @@ namespace FileManager
.GetValidFilename(
Path.Join(directory, replaceFileName(fileNamePart, paramReplacements, LongPath.MaxFilenameLength - fileExtension.Length - 5)) + fileExtension,
replacements,
fileExtension,
returnFirstExisting
);
}

View File

@@ -49,9 +49,11 @@ namespace FileManager
/// <br/>- ensure uniqueness
/// <br/>- enforce max file length
/// </summary>
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, bool returnFirstExisting = false)
public static LongPath GetValidFilename(LongPath path, ReplacementCharacters replacements, string fileExtension, bool returnFirstExisting = false)
{
ArgumentValidator.EnsureNotNull(path, nameof(path));
ArgumentValidator.EnsureNotNull(fileExtension, nameof(fileExtension));
fileExtension = GetStandardizedExtension(fileExtension);
// remove invalid chars
path = GetSafePath(path, replacements);
@@ -60,21 +62,20 @@ namespace FileManager
var dir = Path.GetDirectoryName(path);
dir = dir?.TruncateFilename(LongPath.MaxDirectoryLength) ?? string.Empty;
var extension = Path.GetExtension(path);
var fileName = Path.GetFileName(path);
var extIndex = fileName.LastIndexOf(fileExtension, StringComparison.OrdinalIgnoreCase);
var filenameWithoutExtension = extIndex >= 0 ? fileName.Remove(extIndex, fileExtension.Length) : fileName;
var fileStem
= Path.Combine(dir, filenameWithoutExtension.TruncateFilename(LongPath.MaxFilenameLength - fileExtension.Length))
.TruncateFilename(LongPath.MaxPathLength - fileExtension.Length);
var filename = Path.GetFileNameWithoutExtension(path).TruncateFilename(LongPath.MaxFilenameLength - extension.Length);
var fileStem = Path.Combine(dir, filename);
var fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - extension.Length) + extension;
fullfilename = removeInvalidWhitespace(fullfilename);
var fullfilename = removeInvalidWhitespace(fileStem) + fileExtension;
var i = 0;
while (File.Exists(fullfilename) && !returnFirstExisting)
{
var increm = $" ({++i})";
fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - increm.Length - extension.Length) + increm + extension;
fullfilename = fileStem.TruncateFilename(LongPath.MaxPathLength - increm.Length - fileExtension.Length) + increm + fileExtension;
}
return fullfilename;
@@ -152,7 +153,8 @@ namespace FileManager
/// </summary>
public static string SaferMoveToValidPath(LongPath source, LongPath destination, ReplacementCharacters replacements)
{
destination = GetValidFilename(destination, replacements);
var extension = Path.GetExtension(source);
destination = GetValidFilename(destination, replacements, extension);
SaferMove(source, destination);
return destination;
}

View File

@@ -33,29 +33,34 @@ namespace FileManager
createNewFile();
}
public string GetString(string propertyName)
public string GetString(string propertyName, string defaultValue = null)
{
if (!stringCache.ContainsKey(propertyName))
{
var jObject = readFile();
if (!jObject.ContainsKey(propertyName))
return null;
stringCache[propertyName] = jObject[propertyName].Value<string>();
if (jObject.ContainsKey(propertyName))
stringCache[propertyName] = jObject[propertyName].Value<string>();
else
stringCache[propertyName] = defaultValue;
}
return stringCache[propertyName];
}
public T GetNonString<T>(string propertyName)
public T GetNonString<T>(string propertyName, T defaultValue = default)
{
var obj = GetObject(propertyName);
if (obj is null) return default;
if (obj is null)
{
objectCache[propertyName] = defaultValue;
return defaultValue;
}
if (obj.GetType().IsAssignableTo(typeof(T))) return (T)obj;
if (obj is JObject jObject) return jObject.ToObject<T>();
if (obj is JValue jValue)
{
if (jValue.Type == JTokenType.String && typeof(T).IsAssignableTo(typeof(Enum)))
if (typeof(T).IsAssignableTo(typeof(Enum)))
{
return
Enum.TryParse(typeof(T), jValue.Value<string>(), out var enumVal)

View File

@@ -131,8 +131,6 @@ namespace LibationAvalonia
{
config.Books ??= Path.Combine(Configuration.UserProfile, "Books");
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
var settingsDialog = new SettingsDialog();
desktop.MainWindow = settingsDialog;
settingsDialog.RestoreSizeAndLocation(Configuration.Instance);

View File

@@ -2,11 +2,24 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using DataLayer;
using ReactiveUI;
using System;
namespace LibationAvalonia.Controls
{
public class StarStringConverter : Avalonia.Data.Converters.IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
=> value is Rating rating ? rating.ToStarString() : string.Empty;
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
=> throw new NotImplementedException();
}
public class DataGridMyRatingColumn : DataGridBoundColumn
{
[Avalonia.Data.AssignBinding]
public Avalonia.Data.IBinding BackgroundBinding { get; set; }
private static Rating DefaultRating => new Rating(0, 0, 0);
public DataGridMyRatingColumn()
{
@@ -21,13 +34,19 @@ namespace LibationAvalonia.Controls
IsEditingMode = false
};
ToolTip.SetTip(myRatingElement, "Click to change ratings");
cell?.AttachContextMenu();
if (!IsReadOnly)
ToolTip.SetTip(myRatingElement, "Click to change ratings");
if (Binding != null)
{
myRatingElement.Bind(BindingTarget, Binding);
}
if (BackgroundBinding != null)
{
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
}
return myRatingElement;
}
@@ -39,6 +58,10 @@ namespace LibationAvalonia.Controls
Name = "CellMyRatingEditor",
IsEditingMode = true
};
if (BackgroundBinding != null)
{
myRatingElement.Bind(MyRatingCellEditor.BackgroundProperty, BackgroundBinding);
}
return myRatingElement;
}

View File

@@ -17,7 +17,7 @@
</Grid.Styles>
<TextBlock Grid.Column="0" Grid.Row="0" Name="tblockOverall" Text="Overall:" />
<TextBlock Grid.Column="0" Grid.Row="1" Name="tblockPerform" Text="Perform:" />
<TextBlock Grid.Column="0" Grid.Row="1" Name="tblockPerform" Text="Perform: " />
<TextBlock Grid.Column="0" Grid.Row="2" Name="tblockStory" Text="Story:" />
<Panel Background="Transparent" PointerExited="Panel_PointerExited" Grid.Column="1" Grid.Row="0">

View File

@@ -1,6 +1,8 @@
using Avalonia;
using Avalonia.Controls;
using DataLayer;
using ReactiveUI;
using System;
using System.Linq;
namespace LibationAvalonia.Controls
@@ -9,6 +11,7 @@ namespace LibationAvalonia.Controls
{
private const string SOLID_STAR = "★";
private const string HOLLOW_STAR = "☆";
private const string HALF_STAR = "½";
public static readonly StyledProperty<Rating> RatingProperty =
AvaloniaProperty.Register<MyRatingCellEditor, Rating>(nameof(Rating));
@@ -19,39 +22,41 @@ namespace LibationAvalonia.Controls
public MyRatingCellEditor()
{
InitializeComponent();
var subscriber = this.ObservableForProperty(p => p.Rating).Subscribe(o => DisplayStarRating(o.Value ?? new Rating(0, 0, 0)));
Unloaded += (_, _) => subscriber.Dispose();
if (Design.IsDesignMode)
Rating = new Rating(5, 4, 3);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
private void DisplayStarRating(Rating rating)
{
if (change.Property.Name == nameof(Rating) && Rating is not null)
{
var blankValue = IsEditingMode ? HOLLOW_STAR : string.Empty;
var blankValue = IsEditingMode ? HOLLOW_STAR : string.Empty;
int rating = 0;
foreach (TextBlock star in panelOverall.Children)
star.Tag = star.Text = Rating.OverallRating > rating++ ? SOLID_STAR : blankValue;
string getStar(float score, int starIndex)
=> Math.Floor(score) > starIndex ? SOLID_STAR
: score < starIndex ? blankValue
: score - starIndex < 0.25 ? blankValue
: score - starIndex > 0.75 ? SOLID_STAR
: HALF_STAR;
rating = 0;
foreach (TextBlock star in panelPerform.Children)
star.Tag = star.Text = Rating.PerformanceRating > rating++ ? SOLID_STAR : blankValue;
int starIndex = 0;
foreach (TextBlock star in panelOverall.Children)
star.Tag = star.Text = getStar(rating.OverallRating, starIndex++);
rating = 0;
foreach (TextBlock star in panelStory.Children)
star.Tag = star.Text = Rating.StoryRating > rating++ ? SOLID_STAR : blankValue;
starIndex = 0;
foreach (TextBlock star in panelPerform.Children)
star.Tag = star.Text = getStar(rating.PerformanceRating, starIndex++);
SetVisible();
}
base.OnPropertyChanged(change);
}
starIndex = 0;
foreach (TextBlock star in panelStory.Children)
star.Tag = star.Text = getStar(rating.StoryRating, starIndex++);
private void SetVisible()
{
ratingsGrid.IsEnabled = IsEditingMode;
tblockOverall.IsVisible = panelOverall.IsVisible = IsEditingMode || Rating?.OverallRating > 0;
tblockPerform.IsVisible = panelPerform.IsVisible = IsEditingMode || Rating?.PerformanceRating > 0;
tblockStory.IsVisible = panelStory.IsVisible = IsEditingMode || Rating?.StoryRating > 0;
tblockOverall.IsVisible = panelOverall.IsVisible = IsEditingMode || rating.OverallRating > 0;
tblockPerform.IsVisible = panelPerform.IsVisible = IsEditingMode || rating.PerformanceRating > 0;
tblockStory.IsVisible = panelStory.IsVisible = IsEditingMode || rating.StoryRating > 0;
}
public void Panel_PointerExited(object sender, Avalonia.Input.PointerEventArgs e)

View File

@@ -120,7 +120,7 @@ namespace LibationAvalonia.Dialogs
SuggestedStartLocation = new BclStorageFolder(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)),
FileTypeFilter = new FilePickerFileType[]
{
new("JSON files (*.json)") { Patterns = new[] { "json" } },
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
}
};
@@ -280,7 +280,7 @@ namespace LibationAvalonia.Dialogs
ShowOverwritePrompt = true,
FileTypeChoices = new FilePickerFileType[]
{
new("JSON files (*.json)") { Patterns = new[] { "json" } },
new("JSON files (*.json)") { Patterns = new[] { "*.json" } },
}
};

View File

@@ -27,7 +27,7 @@ namespace LibationAvalonia.Dialogs
userEditTbox = this.FindControl<TextBox>(nameof(userEditTbox));
if (Design.IsDesignMode)
{
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
_ = Configuration.Instance.LibationFiles;
_viewModel = new(Configuration.Instance, Templates.File);
_viewModel.resetTextBox(_viewModel.Template.DefaultTemplate);
Title = $"Edit {_viewModel.Template.Name}";
@@ -162,14 +162,17 @@ namespace LibationAvalonia.Dialogs
Title = chapterName
};
/*
* Path must be rooted for windows to allow long file paths. This is
* only necessary for folder templates because they may contain several
* subdirectories. Without rooting, we won't be allowed to create a
* relative path longer than MAX_PATH.
*/
var books = config.Books;
var folder = Templates.Folder.GetPortionFilename(
libraryBookDto,
//Path must be rooted for windows to allow long file paths. This is
//only necessary for folder templates because they may contain several
//subdirectories. Without rooting, we won't be allowed to create a
//relative path longer than MAX_PATH
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate));
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate), "");
folder = Path.GetRelativePath(books, folder);
@@ -182,7 +185,7 @@ namespace LibationAvalonia.Dialogs
"")
: Templates.File.GetPortionFilename(
libraryBookDto,
isFolder ? config.FileTemplate : workingTemplateText);
isFolder ? config.FileTemplate : workingTemplateText, "");
var ext = config.DecryptToLossy ? "mp3" : "m4b";
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);

View File

@@ -22,7 +22,7 @@ namespace LibationAvalonia.Dialogs
public SettingsDialog()
{
if (Design.IsDesignMode)
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
_ = Configuration.Instance.LibationFiles;
InitializeComponent();
DataContext = settingsDisp = new(config);

View File

@@ -28,8 +28,7 @@ namespace LibationAvalonia
if (Design.IsDesignMode) return;
try
{
FormSizeAndPosition savedState = config.GetNonString<FormSizeAndPosition>(form.GetType().Name);
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.GetType().Name);
if (savedState is null)
return;

View File

@@ -44,8 +44,7 @@ namespace LibationAvalonia.ViewModels
public string Category { get; protected set; }
public string Misc { get; protected set; }
public string Description { get; protected set; }
public string ProductRating { get; protected set; }
public string MyRatingString => MyRating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
public Rating ProductRating { get; protected set; }
protected Rating _myRating;
public Rating MyRating
{
@@ -57,13 +56,6 @@ namespace LibationAvalonia.ViewModels
&& updateReviewTask?.IsCompleted is not false)
{
updateReviewTask = UpdateRating(value);
updateReviewTask.ContinueWith(t =>
{
if (t.Result)
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, value);
this.RaiseAndSetIfChanged(ref _myRating, value);
});
}
}
}
@@ -75,19 +67,24 @@ namespace LibationAvalonia.ViewModels
public abstract bool IsSeries { get; }
public abstract bool IsEpisode { get; }
public abstract bool IsBook { get; }
public abstract double Opacity { get; }
public IBrush BackgroundBrush => IsEpisode ? App.SeriesEntryGridBackgroundBrush : Brushes.Transparent;
#endregion
#region User rating
private Task<bool> updateReviewTask;
private async Task<bool> UpdateRating(Rating rating)
private Task updateReviewTask;
private async Task UpdateRating(Rating rating)
{
var api = await LibraryBook.GetApiAsync();
return await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating);
if (await api.ReviewAsync(Book.AudibleProductId, (int)rating.OverallRating, (int)rating.PerformanceRating, (int)rating.StoryRating))
{
_myRating = rating;
LibraryBook.Book.UpdateUserDefinedItem(Book.UserDefinedItem.Tags, Book.UserDefinedItem.BookStatus, Book.UserDefinedItem.PdfStatus, rating);
}
this.RaisePropertyChanged(nameof(MyRating));
}
#endregion

View File

@@ -53,7 +53,6 @@ namespace LibationAvalonia.ViewModels
public override bool IsSeries => false;
public override bool IsEpisode => Parent is not null;
public override bool IsBook => Parent is null;
public override double Opacity => Book.UserDefinedItem.Tags.ToLower().Contains("hidden") ? 0.4 : 1;
#endregion
@@ -69,7 +68,7 @@ namespace LibationAvalonia.ViewModels
//the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
_myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
PurchaseDate = libraryBook.DateAdded.ToString("d");
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
ProductRating = Book.Rating ?? new Rating(0, 0, 0);
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());
@@ -102,7 +101,6 @@ namespace LibationAvalonia.ViewModels
case nameof(udi.Tags):
Book.UserDefinedItem.Tags = udi.Tags;
this.RaisePropertyChanged(nameof(BookTags));
this.RaisePropertyChanged(nameof(Opacity));
break;
case nameof(udi.BookStatus):
Book.UserDefinedItem.BookStatus = udi.BookStatus;

View File

@@ -31,7 +31,7 @@ namespace LibationAvalonia.ViewModels
Queue.CompletedCountChanged += Queue_CompletedCountChanged;
if (Design.IsDesignMode)
AudibleUtilities.AudibleApiStorage.EnsureAccountsSettingsFileExists();
_ = Configuration.Instance.LibationFiles;
SpeedLimit = Configuration.Instance.DownloadSpeedLimit / 1024m / 1024;
}

View File

@@ -77,7 +77,7 @@ namespace LibationAvalonia.ViewModels
//Run query on new list
FilteredInGridEntries = QueryResults(SOURCE, FilterString);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
await refreshGrid();
}
catch (Exception ex)
@@ -86,6 +86,14 @@ namespace LibationAvalonia.ViewModels
}
}
private async Task refreshGrid()
{
if (GridEntries.IsEditingItem)
await Dispatcher.UIThread.InvokeAsync(GridEntries.CommitEdit);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
}
private static List<GridEntry> CreateGridEntries(IEnumerable<LibraryBook> dbBooks)
{
var geList = dbBooks
@@ -118,10 +126,11 @@ namespace LibationAvalonia.ViewModels
return bookList;
}
public void ToggleSeriesExpanded(SeriesEntry seriesEntry)
public async Task ToggleSeriesExpanded(SeriesEntry seriesEntry)
{
seriesEntry.Liberate.Expanded = !seriesEntry.Liberate.Expanded;
GridEntries.Refresh();
await refreshGrid();
}
#endregion
@@ -140,7 +149,7 @@ namespace LibationAvalonia.ViewModels
FilteredInGridEntries = QueryResults(SOURCE, searchString);
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
await refreshGrid();
}
private bool CollectionFilter(object item)
@@ -176,11 +185,7 @@ namespace LibationAvalonia.ViewModels
if (filterResults is not null && FilteredInGridEntries.Intersect(filterResults).Count() != FilteredInGridEntries.Count)
{
FilteredInGridEntries = filterResults;
if (GridEntries.IsEditingItem)
GridEntries.CommitEdit();
await Dispatcher.UIThread.InvokeAsync(GridEntries.Refresh);
await refreshGrid();
}
}

View File

@@ -49,7 +49,6 @@ namespace LibationAvalonia.ViewModels
public override bool IsSeries => true;
public override bool IsEpisode => false;
public override bool IsBook => false;
public override double Opacity => 1;
#endregion
@@ -71,7 +70,7 @@ namespace LibationAvalonia.ViewModels
//Ratings are changed using Update(), which is a problem for Avalonia data bindings because
//the reference doesn't change. Clone the rating so that it updates within Avalonia properly.
_myRating = new Rating(Book.UserDefinedItem.Rating.OverallRating, Book.UserDefinedItem.Rating.PerformanceRating, Book.UserDefinedItem.Rating.StoryRating);
ProductRating = Book.Rating?.ToStarString()?.DefaultIfNullOrWhiteSpace("");
ProductRating = Book.Rating ?? new Rating(0, 0, 0);
Authors = Book.AuthorNames();
Narrators = Book.NarratorNames();
Category = string.Join(" > ", Book.CategoriesNames());

View File

@@ -6,12 +6,11 @@ using System.Linq;
namespace LibationAvalonia.Views
{
//DONE
public partial class MainWindow
{
private void Configure_ProcessQueue()
{
var collapseState = !Configuration.Instance.GetNonString<bool>(nameof(_viewModel.QueueOpen));
var collapseState = !Configuration.Instance.GetNonString(defaultValue: true, nameof(_viewModel.QueueOpen));
SetQueueCollapseState(collapseState);
}
@@ -51,12 +50,12 @@ namespace LibationAvalonia.Views
private void SetQueueCollapseState(bool collapsed)
{
_viewModel.QueueOpen = !collapsed;
Configuration.Instance.SetNonString(!collapsed, nameof(_viewModel.QueueOpen));
}
public void ToggleQueueHideBtn_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
SetQueueCollapseState(_viewModel.QueueOpen);
Configuration.Instance.SetNonString(_viewModel.QueueOpen, nameof(_viewModel.QueueOpen));
}
}
}

View File

@@ -80,7 +80,7 @@ namespace LibationAvalonia.Views
const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance;
if (config.GetString(ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
if (config.GetString(propertyName: ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
return;
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);

View File

@@ -19,10 +19,12 @@
CanUserReorderColumns="True">
<DataGrid.Styles>
<Style Selector="DataGridCell > Panel">
<Setter Property="Margin" Value="0,1,0,1"/>
<Style Selector="DataGridCell">
<Setter Property="Height" Value="80"/>
</Style>
<Style Selector="DataGridCell > Panel">
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
<Style Selector="DataGridCell > Panel > TextBlock">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
@@ -31,6 +33,10 @@
</Style>
</DataGrid.Styles>
<DataGrid.Resources>
<controls:StarStringConverter x:Key="starStringConverter" />
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn
@@ -73,7 +79,7 @@
<controls:DataGridTemplateColumnExt MinWidth="150" Width="2*" Header="Title" CanUserSort="True" SortMemberPath="Title" ClipboardContentBinding="{Binding Title}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Title}" />
</Panel>
</DataTemplate>
@@ -83,7 +89,7 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Authors" CanUserSort="True" SortMemberPath="Authors" ClipboardContentBinding="{Binding Authors}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Authors}" />
</Panel>
</DataTemplate>
@@ -93,7 +99,7 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Narrators" CanUserSort="True" SortMemberPath="Narrators" ClipboardContentBinding="{Binding Narrators}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Narrators}" />
</Panel>
</DataTemplate>
@@ -103,7 +109,7 @@
<controls:DataGridTemplateColumnExt Width="90" Header="Length" CanUserSort="True" SortMemberPath="Length" ClipboardContentBinding="{Binding Length}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Length}" />
</Panel>
</DataTemplate>
@@ -113,7 +119,7 @@
<controls:DataGridTemplateColumnExt MinWidth="80" Width="1*" Header="Series" CanUserSort="True" SortMemberPath="Series" ClipboardContentBinding="{Binding Series}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Series}" />
</Panel>
</DataTemplate>
@@ -123,7 +129,7 @@
<controls:DataGridTemplateColumnExt MinWidth="100" Width="1*" Header="Description" CanUserSort="True" SortMemberPath="Description" ClipboardContentBinding="{Binding LongDescription}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<Panel Background="{Binding BackgroundBrush}" Tapped="Description_Click" ToolTip.Tip="Click to see full description" >
<TextBlock Text="{Binding Description}" FontSize="11" VerticalAlignment="Top" />
</Panel>
</DataTemplate>
@@ -133,39 +139,45 @@
<controls:DataGridTemplateColumnExt Width="100" Header="Category" CanUserSort="True" SortMemberPath="Category" ClipboardContentBinding="{Binding Category}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Category}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridTemplateColumnExt Width="115" Header="Product&#xA;Rating" CanUserSort="True" SortMemberPath="ProductRating" ClipboardContentBinding="{Binding ProductRating}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<TextBlock Text="{Binding ProductRating}" TextWrapping="NoWrap" FontSize="11" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridMyRatingColumn
Header="Product&#xA;Rating"
IsReadOnly="true"
Width="115"
SortMemberPath="ProductRating" CanUserSort="True"
BackgroundBinding="{Binding BackgroundBrush}"
ClipboardContentBinding="{Binding ProductRating, Converter={StaticResource starStringConverter}}"
Binding="{Binding ProductRating}" />
<controls:DataGridTemplateColumnExt Width="90" Header="Purchase&#xA;Date" CanUserSort="True" SortMemberPath="PurchaseDate" ClipboardContentBinding="{Binding PurchaseDate}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding PurchaseDate}" />
</Panel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumnExt>
<controls:DataGridMyRatingColumn IsReadOnly="false" Width="115" Header="My Rating" CanUserSort="True" SortMemberPath="MyRating" ClipboardContentBinding="{Binding MyRatingString}" Binding="{Binding MyRating, Mode=TwoWay}" />
<controls:DataGridMyRatingColumn
Header="My Rating"
IsReadOnly="false"
Width="115"
SortMemberPath="MyRating" CanUserSort="True"
BackgroundBinding="{Binding BackgroundBrush}"
ClipboardContentBinding="{Binding MyRating, Converter={StaticResource starStringConverter}}"
Binding="{Binding MyRating, Mode=TwoWay}" />
<controls:DataGridTemplateColumnExt Width="135" Header="Misc" CanUserSort="True" SortMemberPath="Misc" ClipboardContentBinding="{Binding Misc}">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Panel Background="{Binding BackgroundBrush}" Opacity="{Binding Opacity}">
<Panel Background="{Binding BackgroundBrush}">
<TextBlock Text="{Binding Misc}" TextWrapping="WrapWithOverflow" FontSize="10" />
</Panel>
</DataTemplate>
@@ -187,6 +199,5 @@
</DataGrid.Columns>
</DataGrid>
</Grid>
</UserControl>

View File

@@ -273,13 +273,13 @@ namespace LibationAvalonia.Views
#region Button Click Handlers
public void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
public async void LiberateButton_Click(object sender, Avalonia.Interactivity.RoutedEventArgs args)
{
var button = args.Source as Button;
if (button.DataContext is SeriesEntry sEntry)
{
_viewModel.ToggleSeriesExpanded(sEntry);
await _viewModel.ToggleSeriesExpanded(sEntry);
//Expanding and collapsing reset the list, which will cause focus to shift
//to the topright cell. Reset focus onto the clicked button's cell.

View File

@@ -20,9 +20,9 @@ namespace LibationFileManager
private PersistentDictionary persistentDictionary;
public T GetNonString<T>([CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString<T>(propertyName);
public T GetNonString<T>(T defaultValue, [CallerMemberName] string propertyName = "") => persistentDictionary.GetNonString(propertyName, defaultValue);
public object GetObject([CallerMemberName] string propertyName = "") => persistentDictionary.GetObject(propertyName);
public string GetString([CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName);
public string GetString(string defaultValue = null, [CallerMemberName] string propertyName = "") => persistentDictionary.GetString(propertyName, defaultValue);
public void SetNonString(object newValue, [CallerMemberName] string propertyName = "")
{
var existing = getExistingValue(propertyName);
@@ -74,77 +74,82 @@ namespace LibationFileManager
public bool Exists(string propertyName) => persistentDictionary.Exists(propertyName);
[Description("Set cover art as the folder's icon. (Windows only)")]
public bool UseCoverAsFolderIcon { get => GetNonString<bool>(); set => SetNonString(value); }
public bool UseCoverAsFolderIcon { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Use the beta version of Libation\r\nNew and experimental features, but probably buggy.\r\n(requires restart to take effect)")]
public bool BetaOptIn { get => GetNonString<bool>(); set => SetNonString(value); }
public bool BetaOptIn { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Location for book storage. Includes destination of newly liberated books")]
public string Books { get => GetString(); set => SetString(value); }
// temp/working dir(s) should be outside of dropbox
[Description("Temporary location of files while they're in process of being downloaded and decrypted.\r\nWhen decryption is complete, the final file will be in Books location\r\nRecommend not using a folder which is backed up real time. Eg: Dropbox, iCloud, Google Drive")]
public string InProgress { get => GetString(); set => SetString(value); }
public string InProgress { get
{
var tempDir = GetString();
return string.IsNullOrWhiteSpace(tempDir) ? WinTemp : tempDir;
}
set => SetString(value); }
[Description("Allow Libation to fix up audiobook metadata")]
public bool AllowLibationFixup { get => GetNonString<bool>(); set => SetNonString(value); }
public bool AllowLibationFixup { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Create a cue sheet (.cue)")]
public bool CreateCueSheet { get => GetNonString<bool>(); set => SetNonString(value); }
public bool CreateCueSheet { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Retain the Aax file after successfully decrypting")]
public bool RetainAaxFile { get => GetNonString<bool>(); set => SetNonString(value); }
public bool RetainAaxFile { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Split my books into multiple files by chapter")]
public bool SplitFilesByChapter { get => GetNonString<bool>(); set => SetNonString(value); }
public bool SplitFilesByChapter { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Merge Opening/End Credits into the following/preceding chapters")]
public bool MergeOpeningAndEndCredits { get => GetNonString<bool>(); set => SetNonString(value); }
public bool MergeOpeningAndEndCredits { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Strip \"(Unabridged)\" from audiobook metadata tags")]
public bool StripUnabridged { get => GetNonString<bool>(); set => SetNonString(value); }
public bool StripUnabridged { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Strip audible branding from the start and end of audiobooks.\r\n(e.g. \"This is Audible\")")]
public bool StripAudibleBrandAudio { get => GetNonString<bool>(); set => SetNonString(value); }
public bool StripAudibleBrandAudio { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Decrypt to lossy format?")]
public bool DecryptToLossy { get => GetNonString<bool>(); set => SetNonString(value); }
public bool DecryptToLossy { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Lame encoder target. true = Bitrate, false = Quality")]
public bool LameTargetBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
public bool LameTargetBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Lame encoder downsamples to mono")]
public bool LameDownsampleMono { get => GetNonString<bool>(); set => SetNonString(value); }
public bool LameDownsampleMono { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Lame target bitrate [16,320]")]
public int LameBitrate { get => GetNonString<int>(); set => SetNonString(value); }
public int LameBitrate { get => GetNonString(defaultValue: 64); set => SetNonString(value); }
[Description("Restrict encoder to constant bitrate?")]
public bool LameConstantBitrate { get => GetNonString<bool>(); set => SetNonString(value); }
public bool LameConstantBitrate { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Match the source bitrate?")]
public bool LameMatchSourceBR { get => GetNonString<bool>(); set => SetNonString(value); }
public bool LameMatchSourceBR { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Lame target VBR quality [10,100]")]
public int LameVBRQuality { get => GetNonString<int>(); set => SetNonString(value); }
public int LameVBRQuality { get => GetNonString(defaultValue: 2); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString<EquatableDictionary<string, bool>>()?.Clone() ?? new(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and bool indicating its column's visibility in ProductsGrid")]
public Dictionary<string, bool> GridColumnsVisibilities { get => GetNonString(defaultValue: new EquatableDictionary<string, bool>()).Clone(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString<EquatableDictionary<string, int>>()?.Clone() ?? new(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's display index in ProductsGrid")]
public Dictionary<string, int> GridColumnsDisplayIndices { get => GetNonString(defaultValue: new EquatableDictionary<string, int>()).Clone(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public Dictionary<string, int> GridColumnsWidths { get => GetNonString<EquatableDictionary<string, int>>()?.Clone() ?? new(); set => SetNonString(value); }
[Description("A Dictionary of GridView data property names and int indicating its column's width in ProductsGrid")]
public Dictionary<string, int> GridColumnsWidths { get => GetNonString(defaultValue: new EquatableDictionary<string, int>()).Clone(); set => SetNonString(value); }
[Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt { get => GetNonString<bool>(); set => SetNonString(value); }
[Description("Save cover image alongside audiobook?")]
public bool DownloadCoverArt { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Download clips and bookmarks?")]
public bool DownloadClipsBookmarks { get => GetNonString<bool>(); set => SetNonString(value); }
public bool DownloadClipsBookmarks { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("File format to save clips and bookmarks")]
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString<ClipBookmarkFormat>(); set => SetNonString(value); }
public ClipBookmarkFormat ClipsBookmarksFileFormat { get => GetNonString(defaultValue: ClipBookmarkFormat.CSV); set => SetNonString(value); }
[JsonConverter(typeof(StringEnumConverter))]
public enum ClipBookmarkFormat
@@ -171,33 +176,33 @@ namespace LibationFileManager
}
[Description("When liberating books and there is an error, Libation should:")]
public BadBookAction BadBook { get => GetNonString<BadBookAction>(); set => SetNonString(value); }
public BadBookAction BadBook { get => GetNonString(defaultValue: BadBookAction.Ask); set => SetNonString(value); }
[Description("Show number of newly imported titles? When unchecked, no pop-up will appear after library scan.")]
public bool ShowImportedStats { get => GetNonString<bool>(); set => SetNonString(value); }
public bool ShowImportedStats { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Import episodes? (eg: podcasts) When unchecked, episodes will not be imported into Libation.")]
public bool ImportEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
public bool ImportEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Download episodes? (eg: podcasts). When unchecked, episodes already in Libation will not be downloaded.")]
public bool DownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
public bool DownloadEpisodes { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Automatically run periodic scans in the background?")]
public bool AutoScan { get => GetNonString<bool>(); set => SetNonString(value); }
public bool AutoScan { get => GetNonString(defaultValue: true); set => SetNonString(value); }
[Description("Auto download books? After scan, download new books in 'checked' accounts.")]
// poorly named setting. Should just be 'AutoDownload'. It is NOT episode specific
public bool AutoDownloadEpisodes { get => GetNonString<bool>(); set => SetNonString(value); }
public bool AutoDownloadEpisodes { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Save all podcast episodes in a series to the series parent folder?")]
public bool SavePodcastsToParentFolder { get => GetNonString<bool>(); set => SetNonString(value); }
public bool SavePodcastsToParentFolder { get => GetNonString(defaultValue: false); set => SetNonString(value); }
[Description("Global download speed limit in bytes per second.")]
public long DownloadSpeedLimit
{
get
{
var limit = GetNonString<long>();
var limit = GetNonString(defaultValue: 0L);
return limit <= 0 ? 0 : Math.Max(limit, AaxDecrypter.NetworkFileStream.MIN_BYTES_PER_SECOND);
}
set
@@ -210,42 +215,41 @@ namespace LibationFileManager
#region templates: custom file naming
[Description("Edit how filename characters are replaced")]
public ReplacementCharacters ReplacementCharacters { get => GetNonString<ReplacementCharacters>(); set => SetNonString(value); }
public ReplacementCharacters ReplacementCharacters { get => GetNonString(defaultValue: ReplacementCharacters.Default); set => SetNonString(value); }
[Description("How to format the folders in which files will be saved")]
public string FolderTemplate
{
get => getTemplate(nameof(FolderTemplate), Templates.Folder);
set => setTemplate(nameof(FolderTemplate), Templates.Folder, value);
get => Templates.Folder.GetValid(GetString(defaultValue: Templates.Folder.DefaultTemplate));
set => setTemplate(Templates.Folder, value);
}
[Description("How to format the saved pdf and audio files")]
public string FileTemplate
{
get => getTemplate(nameof(FileTemplate), Templates.File);
set => setTemplate(nameof(FileTemplate), Templates.File, value);
get => Templates.File.GetValid(GetString(defaultValue: Templates.File.DefaultTemplate));
set => setTemplate(Templates.File, value);
}
[Description("How to format the saved audio files when split by chapters")]
public string ChapterFileTemplate
{
get => getTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile);
set => setTemplate(nameof(ChapterFileTemplate), Templates.ChapterFile, value);
get => Templates.ChapterFile.GetValid(GetString(defaultValue: Templates.ChapterFile.DefaultTemplate));
set => setTemplate(Templates.ChapterFile, value);
}
[Description("How to format the file's Tile stored in metadata")]
public string ChapterTitleTemplate
{
get => getTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle);
set => setTemplate(nameof(ChapterTitleTemplate), Templates.ChapterTitle, value);
get => Templates.ChapterTitle.GetValid(GetString(defaultValue: Templates.ChapterTitle.DefaultTemplate));
set => setTemplate(Templates.ChapterTitle, value);
}
private string getTemplate(string settingName, Templates templ) => templ.GetValid(GetString(settingName));
private void setTemplate(string settingName, Templates templ, string newValue)
private void setTemplate(Templates templ, string newValue, [CallerMemberName] string propertyName = "")
{
var template = newValue?.Trim();
if (templ.IsValid(template))
SetString(template, settingName);
SetString(template, propertyName);
}
#endregion
}

View File

@@ -12,7 +12,7 @@ namespace LibationFileManager
private class EquatableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public EquatableDictionary() { }
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
public EquatableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keyValuePairs) : base(keyValuePairs) { }
public EquatableDictionary<TKey, TValue> Clone() => new(this);
public override bool Equals(object obj)
{

View File

@@ -24,9 +24,6 @@ namespace LibationFileManager
if (booksDir is null || !Directory.Exists(booksDir))
return false;
if (string.IsNullOrWhiteSpace(pDic.GetString(nameof(InProgress))))
return false;
return true;
}

View File

@@ -79,23 +79,36 @@ namespace LibationFileManager
{
private readonly Dictionary<string, List<Delegate>> propertyChangedActions = new();
private readonly Dictionary<string, List<Delegate>> propertyChangingActions = new();
private readonly List<(PropertyChangedEventHandlerEx subscriber, PropertyChangedEventHandlerEx wrapper)> changedFilters = new();
private readonly List<(PropertyChangingEventHandlerEx subscriber, PropertyChangingEventHandlerEx wrapper)> changingFilters = new();
public PropertyChangeFilter()
protected void OnPropertyChanged(string propertyName, object newValue)
{
PropertyChanging += Configuration_PropertyChanging;
PropertyChanged += Configuration_PropertyChanged;
if (propertyChangedActions.ContainsKey(propertyName))
{
//Invoke observables registered for propertyName
foreach (var action in propertyChangedActions[propertyName])
action.DynamicInvoke(newValue);
}
_propertyChanged?.Invoke(this, new(propertyName, newValue));
}
protected void OnPropertyChanging(string propertyName, object oldValue, object newValue)
{
if (propertyChangingActions.ContainsKey(propertyName))
{
//Invoke observables registered for propertyName
foreach (var action in propertyChangingActions[propertyName])
action.DynamicInvoke(oldValue, newValue);
}
_propertyChanging?.Invoke(this, new(propertyName, oldValue, newValue));
}
#region Events
protected void OnPropertyChanged(string propertyName, object newValue)
=> _propertyChanged?.Invoke(this, new(propertyName, newValue));
protected void OnPropertyChanging(string propertyName, object oldValue, object newValue)
=> _propertyChanging?.Invoke(this, new(propertyName, oldValue, newValue));
private PropertyChangedEventHandlerEx _propertyChanged;
private PropertyChangingEventHandlerEx _propertyChanging;
@@ -255,28 +268,6 @@ namespace LibationFileManager
throw new InvalidCastException($"{nameof(Configuration)}.{propertyName} is {propertyInfo.PropertyType}, but parameter is {typeof(T)}.");
}
private void Configuration_PropertyChanged(object sender, PropertyChangedEventArgsEx e)
{
if (propertyChangedActions.ContainsKey(e.PropertyName))
{
foreach (var action in propertyChangedActions[e.PropertyName])
{
action.DynamicInvoke(e.NewValue);
}
}
}
private void Configuration_PropertyChanging(object sender, PropertyChangingEventArgsEx e)
{
if (propertyChangingActions.ContainsKey(e.PropertyName))
{
foreach (var action in propertyChangingActions[e.PropertyName])
{
action.DynamicInvoke(e.OldValue, e.NewValue);
}
}
}
private class Unsubscriber : IDisposable
{
private List<Delegate> _observers;

View File

@@ -99,11 +99,11 @@ namespace LibationFileManager
/// <summary>
/// EditTemplateDialog: Get template generated filename for portion of path
/// </summary>
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template)
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, string fileExtension)
=> string.IsNullOrWhiteSpace(template)
? ""
: getFileNamingTemplate(libraryBookDto, template, null, null)
.GetFilePath(Configuration.Instance.ReplacementCharacters).PathWithoutPrefix;
: getFileNamingTemplate(libraryBookDto, template, null, fileExtension)
.GetFilePath(Configuration.Instance.ReplacementCharacters, fileExtension).PathWithoutPrefix;
private static Regex ifSeriesRegex { get; } = new Regex("<if series->(.*?)<-if series>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
@@ -215,7 +215,7 @@ namespace LibationFileManager
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
public string GetFilename(LibraryBookDto libraryBookDto, string baseDir = null)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FolderTemplate, baseDir ?? AudibleFileStorage.BooksDirectory, null)
.GetFilePath(Configuration.Instance.ReplacementCharacters);
.GetFilePath(Configuration.Instance.ReplacementCharacters, string.Empty);
#endregion
}
@@ -238,7 +238,7 @@ namespace LibationFileManager
/// <summary>USES LIVE CONFIGURATION VALUES</summary>
public string GetFilename(LibraryBookDto libraryBookDto, string dirFullPath, string extension, bool returnFirstExisting = false)
=> getFileNamingTemplate(libraryBookDto, Configuration.Instance.FileTemplate, dirFullPath, extension)
.GetFilePath(Configuration.Instance.ReplacementCharacters, returnFirstExisting);
.GetFilePath(Configuration.Instance.ReplacementCharacters, extension, returnFirstExisting);
#endregion
}
@@ -273,19 +273,20 @@ namespace LibationFileManager
public string GetFilename(LibraryBookDto libraryBookDto, AaxDecrypter.MultiConvertFileProperties props)
=> GetPortionFilename(libraryBookDto, Configuration.Instance.ChapterFileTemplate, props, AudibleFileStorage.DecryptInProgressDirectory);
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
public string GetPortionFilename(LibraryBookDto libraryBookDto, string template, AaxDecrypter.MultiConvertFileProperties props, string fullDirPath, ReplacementCharacters replacements = null)
{
if (string.IsNullOrWhiteSpace(template)) return string.Empty;
replacements ??= Configuration.Instance.ReplacementCharacters;
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, Path.GetExtension(props.OutputFileName));
var fileExtension = Path.GetExtension(props.OutputFileName);
var fileNamingTemplate = getFileNamingTemplate(libraryBookDto, template, fullDirPath, fileExtension);
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChCount, props.PartsTotal);
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber, props.PartsPosition);
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChNumber0, FileUtility.GetSequenceFormatted(props.PartsPosition, props.PartsTotal));
fileNamingTemplate.AddParameterReplacement(TemplateTags.ChTitle, props.Title ?? "");
return fileNamingTemplate.GetFilePath(replacements).PathWithoutPrefix;
return fileNamingTemplate.GetFilePath(replacements, fileExtension).PathWithoutPrefix;
}
#endregion
}

View File

@@ -98,15 +98,16 @@ namespace LibationWinForms.Dialogs
};
/*
* Path must be rooted for windows to allow long file paths. This is
* only necessary for folder templates because they may contain several
* subdirectories. Without rooting, we won't be allowed to create a
* relative path longer than MAX_PATH.
*/
var books = config.Books;
var folder = Templates.Folder.GetPortionFilename(
libraryBookDto,
//Path must be rooted for windows to allow long file paths. This is
//only necessary for folder templates because they may contain several
//subdirectories. Without rooting, we won't be allowed to create a
//relative path longer than MAX_PATH
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate));
libraryBookDto,
Path.Combine(books, isFolder ? workingTemplateText : config.FolderTemplate), "");
folder = Path.GetRelativePath(books, folder);
@@ -119,7 +120,7 @@ namespace LibationWinForms.Dialogs
"")
: Templates.File.GetPortionFilename(
libraryBookDto,
isFolder ? config.FileTemplate : workingTemplateText);
isFolder ? config.FileTemplate : workingTemplateText, "");
var ext = config.DecryptToLossy ? "mp3" : "m4b";
var chapterTitle = Templates.ChapterTitle.GetPortionTitle(libraryBookDto, workingTemplateText, partFileProperties);

View File

@@ -208,11 +208,7 @@
#endregion
private System.Windows.Forms.Label label1;
private System.Windows.Forms.TextBox releaseNotesTbox;
private System.Windows.Forms.GroupBox groupBox1;
private System.Windows.Forms.LinkLabel linkLabel3;
private System.Windows.Forms.LinkLabel linkLabel2;
private System.Windows.Forms.LinkLabel packageDlLink;
private System.Windows.Forms.Button dontRemindBtn;
private System.Windows.Forms.Button yesBtn;

View File

@@ -30,7 +30,7 @@ namespace LibationWinForms.Dialogs
private void UpgradeNotificationDialog_Load(object sender, EventArgs e)
{
//This dialog starts before Form1, soposition it at the center of where Form1 will be.
var savedState = Configuration.Instance.GetNonString<FormSizeAndPosition>(nameof(Form1));
var savedState = Configuration.Instance.GetNonString<FormSizeAndPosition>(defaultValue: null, nameof(Form1));
if (savedState is null) return;

View File

@@ -16,7 +16,7 @@ namespace LibationWinForms
{
processBookQueue1.popoutBtn.Click += ProcessBookQueue1_PopOut;
splitContainer1.Panel2MinSize = 350;
var coppalseState = Configuration.Instance.GetNonString<bool>(nameof(splitContainer1.Panel2Collapsed));
var coppalseState = Configuration.Instance.GetNonString(defaultValue: false, nameof(splitContainer1.Panel2Collapsed));
WidthChange = splitContainer1.Panel2.Width + splitContainer1.SplitterWidth;
int width = this.Width;
SetQueueCollapseState(coppalseState);

View File

@@ -21,7 +21,7 @@ namespace LibationWinForms
public static void RestoreSizeAndLocation(this Form form, Configuration config)
{
FormSizeAndPosition savedState = config.GetNonString<FormSizeAndPosition>(form.Name);
var savedState = config.GetNonString<FormSizeAndPosition>(defaultValue: null, form.Name);
if (savedState is null)
return;

View File

@@ -144,7 +144,6 @@ namespace LibationWinForms
// INIT DEFAULT SETTINGS
// if 'new user' was clicked, or if 'returning user' chose new install: show basic settings dialog
config.Books ??= Path.Combine(defaultLibationFilesDir, "Books");
AppScaffolding.LibationScaffolding.PopulateMissingConfigValues(config);
if (new SettingsDialog().ShowDialog() != DialogResult.OK)
{

View File

@@ -29,7 +29,7 @@ namespace LibationWinForms
const string ignoreUpdate = "IgnoreUpdate";
var config = Configuration.Instance;
if (config.GetString(ignoreUpdate) == args.CurrentVersion)
if (config.GetString(propertyName: ignoreUpdate) == args.CurrentVersion)
return;
var notificationResult = new UpgradeNotificationDialog(upgradeProperties).ShowDialog();

View File

@@ -33,12 +33,13 @@ namespace FileNamingTemplateTests
{
var template = $"<title> [<id>]";
var fullfilename = Path.Combine(dirFullPath, template + FileUtility.GetStandardizedExtension(extension));
extension = FileUtility.GetStandardizedExtension(extension);
var fullfilename = Path.Combine(dirFullPath, template + extension);
var fileNamingTemplate = new FileNamingTemplate(fullfilename);
fileNamingTemplate.AddParameterReplacement("title", filename);
fileNamingTemplate.AddParameterReplacement("id", metadataSuffix);
return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
return fileNamingTemplate.GetFilePath(Replacements, extension).PathWithoutPrefix;
}
[TestMethod]
@@ -57,12 +58,13 @@ namespace FileNamingTemplateTests
// 100-999 => 001-999
var chapterCountLeadingZeros = partsPosition.ToString().PadLeft(partsTotal.ToString().Length, '0');
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + Path.GetExtension(originalPath);
var estension = Path.GetExtension(originalPath);
var t = Path.ChangeExtension(originalPath, null) + " - <chapter> - <title>" + estension;
var fileNamingTemplate = new FileNamingTemplate(t);
fileNamingTemplate.AddParameterReplacement("chapter", chapterCountLeadingZeros);
fileNamingTemplate.AddParameterReplacement("title", suffix);
return fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix;
return fileNamingTemplate.GetFilePath(Replacements, estension).PathWithoutPrefix;
}
[TestMethod]
@@ -74,7 +76,7 @@ namespace FileNamingTemplateTests
{
var fileNamingTemplate = new FileNamingTemplate(inStr);
fileNamingTemplate.AddParameterReplacement("title", @"s\l/a\s/h\e/s");
fileNamingTemplate.GetFilePath(Replacements).PathWithoutPrefix.Should().Be(outStr);
fileNamingTemplate.GetFilePath(Replacements, "txt").PathWithoutPrefix.Should().Be(outStr);
}
}
}

View File

@@ -197,30 +197,30 @@ namespace FileUtilityTests
[TestMethod]
// dot-files
[DataRow(@"C:\a bc\x y z\.f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/.f i l e.txt", PlatformID.Unix)]
[DataRow(@"C:\a bc\x y z\.f i l e.txt", "txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/.f i l e.txt", "txt", PlatformID.Unix)]
// dot-folders
[DataRow(@"C:\a bc\.x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/.x y z/f i l e.txt", PlatformID.Unix)]
public void Valid(string input, PlatformID platformID) => Tests(input, input, platformID);
[DataRow(@"C:\a bc\.x y z\f i l e.txt", "txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/.x y z/f i l e.txt", "txt", PlatformID.Unix)]
public void Valid(string input, string extension, PlatformID platformID) => Tests(input, extension, input, platformID);
[TestMethod]
// folder spaces
[DataRow(@"C:\ a bc \x y z ", @"C:\a bc\x y z", PlatformID.Win32NT)]
[DataRow(@"/ a bc /x y z ", @"/a bc/x y z", PlatformID.Unix)]
[DataRow(@"C:\ a bc \x y z ","", @"C:\a bc\x y z", PlatformID.Win32NT)]
[DataRow(@"/ a bc /x y z ", "", @"/a bc/x y z", PlatformID.Unix)]
// file spaces
[DataRow(@"C:\a bc\x y z\ f i l e.txt ", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/ f i l e.txt ", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
[DataRow(@"C:\a bc\x y z\ f i l e.txt ", "txt", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/ f i l e.txt ", "txt", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
// eliminate beginning space and end dots and spaces
[DataRow(@"C:\a bc\ . . . x y z . . . \f i l e.txt", @"C:\a bc\. . . x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/ . . . x y z . . . /f i l e.txt", @"/a bc/. . . x y z/f i l e.txt", PlatformID.Unix)]
[DataRow(@"C:\a bc\ . . . x y z . . . \f i l e.txt", "txt", @"C:\a bc\. . . x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/ . . . x y z . . . /f i l e.txt", "txt", @"/a bc/. . . x y z/f i l e.txt", PlatformID.Unix)]
// file end dots
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/f i l e.txt . . .", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
public void Tests(string input, string expected, PlatformID platformID)
[DataRow(@"C:\a bc\x y z\f i l e.txt . . .", "txt", @"C:\a bc\x y z\f i l e.txt", PlatformID.Win32NT)]
[DataRow(@"/a bc/x y z/f i l e.txt . . .", "txt", @"/a bc/x y z/f i l e.txt", PlatformID.Unix)]
public void Tests(string input, string extension, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
FileUtility.GetValidFilename(input, Replacements).PathWithoutPrefix.Should().Be(expected);
FileUtility.GetValidFilename(input, Replacements, extension).PathWithoutPrefix.Should().Be(expected);
}
}

View File

@@ -81,23 +81,25 @@ namespace TemplatesTests
=> Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension);
[TestMethod]
[DataRow("f.txt", @"C:\foo\bar", null, @"C:\foo\bar\f.txt", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", null, @"/foo/bar/f.txt", PlatformID.Unix)]
[DataRow("f.txt", @"C:\foo\bar", "ext", @"C:\foo\bar\f.txt.ext", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", "ext", @"/foo/bar/f.txt.ext", PlatformID.Unix)]
[DataRow("f", @"C:\foo\bar", "ext", @"C:\foo\bar\f.ext", PlatformID.Win32NT)]
[DataRow("f", @"/foo/bar", "ext", @"/foo/bar/f.ext", PlatformID.Unix)]
[DataRow("<id>", @"C:\foo\bar", "ext", @"C:\foo\bar\asin.ext", PlatformID.Win32NT)]
[DataRow("<id>", @"/foo/bar", "ext", @"/foo/bar/asin.ext", PlatformID.Unix)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\128 - 44100 - 2.ext", PlatformID.Win32NT)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"/foo/bar", "ext", @"/foo/bar/128 - 44100 - 2.ext", PlatformID.Unix)]
[DataRow("<year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\2017 - 2.ext", PlatformID.Win32NT)]
[DataRow("<year> - <channels>", @"/foo/bar", "ext", @"/foo/bar/2017 - 2.ext", PlatformID.Unix)]
public void Tests(string template, string dirFullPath, string extension, string expected, PlatformID platformID)
[DataRow("f.txt", @"C:\foo\bar", "", @"C:\foo\bar\f.txt", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", "", @"/foo/bar/f.txt", PlatformID.Unix)]
[DataRow("f.txt", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.txt.ext", PlatformID.Win32NT)]
[DataRow("f.txt", @"/foo/bar", ".ext", @"/foo/bar/f.txt.ext", PlatformID.Unix)]
[DataRow("f", @"C:\foo\bar", ".ext", @"C:\foo\bar\f.ext", PlatformID.Win32NT)]
[DataRow("f", @"/foo/bar", ".ext", @"/foo/bar/f.ext", PlatformID.Unix)]
[DataRow("<id>", @"C:\foo\bar", ".ext", @"C:\foo\bar\asin.ext", PlatformID.Win32NT)]
[DataRow("<id>", @"/foo/bar", ".ext", @"/foo/bar/asin.ext", PlatformID.Unix)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\128 - 44100 - 2.ext", PlatformID.Win32NT)]
[DataRow("<bitrate> - <samplerate> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/128 - 44100 - 2.ext", PlatformID.Unix)]
[DataRow("<year> - <channels>", @"C:\foo\bar", ".ext", @"C:\foo\bar\2017 - 2.ext", PlatformID.Win32NT)]
[DataRow("<year> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/2017 - 2.ext", PlatformID.Unix)]
[DataRow("(000.0) <year> - <channels>", @"C:\foo\bar", "ext", @"C:\foo\bar\(000.0) 2017 - 2.ext", PlatformID.Win32NT)]
[DataRow("(000.0) <year> - <channels>", @"/foo/bar", ".ext", @"/foo/bar/(000.0) 2017 - 2.ext", PlatformID.Unix)]
public void Tests(string template, string dirFullPath, string extension, string expected, PlatformID platformID)
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), template, dirFullPath, extension)
.GetFilePath(Replacements)
.GetFilePath(Replacements, extension)
.PathWithoutPrefix
.Should().Be(expected);
}
@@ -109,7 +111,7 @@ namespace TemplatesTests
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series-><-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.GetFilePath(Replacements, ".ext")
.PathWithoutPrefix
.Should().Be(expected);
}
@@ -121,7 +123,7 @@ namespace TemplatesTests
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(null), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.GetFilePath(Replacements, ".ext")
.PathWithoutPrefix
.Should().Be(expected);
}
@@ -133,7 +135,7 @@ namespace TemplatesTests
{
if (Environment.OSVersion.Platform == platformID)
Templates.getFileNamingTemplate(GetLibraryBook(), "foo<if series->-<series>-<id>-<-if series>bar", directory, "ext")
.GetFilePath(Replacements)
.GetFilePath(Replacements, ".ext")
.PathWithoutPrefix
.Should().Be(expected);
}