Merge pull request #489 from Mbucari/master
Mac App Bundle and added mp3 conversion support on mac
This commit is contained in:
commit
bb508c0718
2
.github/workflows/build-linux.yml
vendored
2
.github/workflows/build-linux.yml
vendored
@ -66,7 +66,7 @@ jobs:
|
|||||||
id: zip
|
id: zip
|
||||||
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}
|
working-directory: ./Source/bin/Publish/${{ matrix.os }}-${{ matrix.release_name }}
|
||||||
run: |
|
run: |
|
||||||
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll" "ZipExtractor.exe")
|
delfiles=("libmp3lame.x86.dll" "libmp3lame.x64.dll" "ffmpegaac.x86.dll" "ffmpegaac.x64.dll")
|
||||||
for n in "${delfiles[@]}"; do rm "$n"; done
|
for n in "${delfiles[@]}"; do rm "$n"; done
|
||||||
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')"
|
osbuild="$(echo '${{ matrix.os }}' | tr '[:upper:]' '[:lower:]')"
|
||||||
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}"
|
artifact="Libation.${{ steps.get_version.outputs.version }}-${osbuild}-${{ matrix.release_name }}"
|
||||||
|
|||||||
43
.github/workflows/bundle-linux.yml
vendored
Normal file
43
.github/workflows/bundle-linux.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
# build-linux.yml
|
||||||
|
# Reusable workflow that builds the Libation installation bundles for Linux and MacOS.
|
||||||
|
---
|
||||||
|
name: bundle-linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
type: string
|
||||||
|
description: 'Version number'
|
||||||
|
required: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bundle:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [linux, macos]
|
||||||
|
release_name: [chardonnay]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download Artifact
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: "Libation.${{ inputs.version }}-${{ matrix.os }}-${{ matrix.release_name }}.tar.gz"
|
||||||
|
|
||||||
|
- name: Build bundle
|
||||||
|
id: build
|
||||||
|
run: |
|
||||||
|
SCRIPT=targz2${{ matrix.os }}bundle.sh
|
||||||
|
chmod +rwx ./Scripts/${SCRIPT}
|
||||||
|
./Scripts/${SCRIPT} "Libation.${{ inputs.version }}-${{ matrix.os }}-${{ matrix.release_name }}.tar.gz" ${{ inputs.version }}
|
||||||
|
artifact=$(ls ./bundle)
|
||||||
|
echo "artifact=${artifact}" >> "${GITHUB_OUTPUT}"
|
||||||
|
|
||||||
|
- name: Publish bundle
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ steps.build.outputs.artifact }}
|
||||||
|
path: ./bundle/${{ steps.build.outputs.artifact }}
|
||||||
|
if-no-files-found: error
|
||||||
38
.github/workflows/deb.yml
vendored
38
.github/workflows/deb.yml
vendored
@ -1,38 +0,0 @@
|
|||||||
# deb.yml
|
|
||||||
# Reusable workflow that builds the Linux Debian package.
|
|
||||||
---
|
|
||||||
name: 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@v3
|
|
||||||
with:
|
|
||||||
name: "${{ env.FILE_NAME }}.tar.gz"
|
|
||||||
|
|
||||||
- name: Build .deb
|
|
||||||
id: deb
|
|
||||||
run: |
|
|
||||||
./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
|
|
||||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@ -34,14 +34,14 @@ jobs:
|
|||||||
version_override: ${{ needs.prerelease.outputs.version }}
|
version_override: ${{ needs.prerelease.outputs.version }}
|
||||||
run_unit_tests: false
|
run_unit_tests: false
|
||||||
|
|
||||||
deb:
|
bundle:
|
||||||
needs: [prerelease,build]
|
needs: [prerelease,build]
|
||||||
uses: ./.github/workflows/deb.yml
|
uses: ./.github/workflows/bundle-linux.yml
|
||||||
with:
|
with:
|
||||||
version: ${{ needs.prerelease.outputs.version }}
|
version: ${{ needs.prerelease.outputs.version }}
|
||||||
|
|
||||||
release:
|
release:
|
||||||
needs: [prerelease,build,deb]
|
needs: [prerelease,build,bundle]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
@ -53,7 +53,7 @@ jobs:
|
|||||||
id: release
|
id: release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v1
|
||||||
with:
|
with:
|
||||||
name: Libation ${{ needs.prerelease.outputs.version }}
|
name: Libation v${{ needs.prerelease.outputs.version }}
|
||||||
body: <Put a body here>
|
body: <Put a body here>
|
||||||
draft: true
|
draft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
"WindowsClassic": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-classic\\.zip",
|
||||||
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
"WindowsAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-win(dows)?-chardonnay\\.zip",
|
||||||
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay",
|
"LinuxAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-linux-chardonnay\\.deb",
|
||||||
"MacOSAvalonia": "Libation\\.\\d+\\.\\d+\\.\\d+-macos-chardonnay"
|
"MacOSAvalonia": "Libation\\.app-macOS-x64-\\d+\\.\\d+\\.\\d+\\.tgz"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,40 +4,36 @@
|
|||||||
...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.
|
||||||
|
|
||||||
## Install Libation
|
## Install Libation
|
||||||
|
|
||||||
- Download latest MacOS zip to downloads folder
|
- Download the `Libation.app-macOS-x64-x.x.x.tgz` file from the latest release and extract it.
|
||||||
- Extract and rename folder to Libation
|
- Move the extracted Libation app bundle to your applications folder.
|
||||||
- in terminal type cd and then drag your folder of libation to terminal so it looks like `cd/users/YourName/Downloads/Libation`
|
- Open a terminal (Go > Utilities > Terminal)
|
||||||
- Type following commands
|
- Copy/paste/run the following command (you'll be prompted to enter your password)
|
||||||
|
```Console
|
||||||
|
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!
|
||||||
|
|
||||||
```console
|
## Running Hangover
|
||||||
chmod +x ./Libation
|
|
||||||
sudo spctl --add --label "Libation" ./Libation
|
Libation comes with a recovery app called Hangover. You can start it by running this command:
|
||||||
./Libation
|
```Console
|
||||||
|
open /Applications/Libation.app --args hangover
|
||||||
```
|
```
|
||||||
|
|
||||||
## Trouble with Gatekeeper?
|
## Runnign LibationCli
|
||||||
|
|
||||||
If Gatekeeper is giving you trouble with Libation:
|
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
|
||||||
|
open /Applications/Libation.app --args cli
|
||||||
|
```
|
||||||
|
To use LibationCli from an unsandboxed terminal, you must disable gatekeeper again and run the program directly at `/Applications/Libation.app/Contents/MacOS/LibationCli`
|
||||||
|
|
||||||
Disable the block
|
Then use `./LibationCli` to execute a command.
|
||||||
|
|
||||||
`sudo spctl --master-disable`
|
|
||||||
|
|
||||||
Launch Libation and login, etc. and allow the rules to update then re-enable the block.
|
|
||||||
|
|
||||||
`sudo spctl --master-enable`
|
|
||||||
|
|
||||||
Once Gatekeeper reenabled, you can open Libation again without it being blocked.
|
|
||||||
|
|
||||||
Thanks [joseph-holland](https://github.com/rmcrackan/Libation/issues/327#issuecomment-1268993349)!
|
|
||||||
|
|
||||||
Report bugs to https://github.com/rmcrackan/Libation/issues
|
|
||||||
|
|
||||||
## Get Libation running on Mac
|
## Get Libation running on Mac
|
||||||
|
|
||||||
|
|||||||
8
Scripts/targz2deb.sh → Scripts/targz2linuxbundle.sh
Executable file → Normal file
8
Scripts/targz2deb.sh → Scripts/targz2linuxbundle.sh
Executable file → Normal file
@ -106,7 +106,10 @@ 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
|
||||||
|
|
||||||
# Increase the maximum number of inotify instances
|
# Increase the maximum number of inotify instances
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
# workaround until this file is moved to the user's home directory
|
# workaround until this file is moved to the user's home directory
|
||||||
touch /usr/lib/libation/appsettings.json
|
touch /usr/lib/libation/appsettings.json
|
||||||
@ -130,7 +133,10 @@ chmod +x "$FOLDER_DEBIAN/postinst"
|
|||||||
echo "Creating .deb file..."
|
echo "Creating .deb file..."
|
||||||
dpkg-deb -Zxz --build $FOLDER_MAIN
|
dpkg-deb -Zxz --build $FOLDER_MAIN
|
||||||
|
|
||||||
|
mkdir bundle
|
||||||
|
echo "moving to ./bundle/$FOLDER_MAIN.deb"
|
||||||
|
mv "$FOLDER_MAIN.deb" "./bundle/$FOLDER_MAIN.deb"
|
||||||
|
|
||||||
rm -r "$FOLDER_MAIN"
|
rm -r "$FOLDER_MAIN"
|
||||||
|
|
||||||
echo "Done!"
|
echo "Done!"
|
||||||
|
|
||||||
84
Scripts/targz2macosbundle.sh
Normal file
84
Scripts/targz2macosbundle.sh
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
FILE=$1; shift
|
||||||
|
VERSION=$1; shift
|
||||||
|
|
||||||
|
if [ -z "$FILE" ]
|
||||||
|
then
|
||||||
|
echo "This script must be called with a the Libation macos bin zip file as an argument."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$FILE" ]
|
||||||
|
then
|
||||||
|
echo "The file \"$FILE\" does not exist."
|
||||||
|
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
|
||||||
|
|
||||||
|
BUNDLE="Libation.app"
|
||||||
|
echo "Bundle dir: $BUNDLE"
|
||||||
|
|
||||||
|
if [[ -d "$BUNDLE" ]]
|
||||||
|
then
|
||||||
|
echo "$BUNDLE directory already exists, aborting."
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
BUNDLE_CONTENTS="$BUNDLE/Contents"
|
||||||
|
echo "Bundle Contents dir: $BUNDLE_CONTENTS"
|
||||||
|
|
||||||
|
BUNDLE_RESOURCES="$BUNDLE_CONTENTS/Resources"
|
||||||
|
echo "Resources dir: $BUNDLE_RESOURCES"
|
||||||
|
|
||||||
|
BUNDLE_MACOS="$BUNDLE_CONTENTS/MacOS"
|
||||||
|
echo "MacOS dir: $BUNDLE_MACOS"
|
||||||
|
|
||||||
|
mkdir -p "$BUNDLE_CONTENTS"
|
||||||
|
mkdir -p "$BUNDLE_RESOURCES"
|
||||||
|
mkdir -p "$BUNDLE_MACOS"
|
||||||
|
|
||||||
|
echo "Extracting $FILE to $BUNDLE_MACOS..."
|
||||||
|
tar -xzf ${FILE} -C ${BUNDLE_MACOS}
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then echo "Error extracting ${FILE}"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Copying icon..."
|
||||||
|
cp "$BUNDLE_MACOS/libation.icns" "$BUNDLE_RESOURCES/libation.icns"
|
||||||
|
|
||||||
|
echo "Copying Info.plist file..."
|
||||||
|
cp "$BUNDLE_MACOS/Info.plist" "$BUNDLE_CONTENTS/Info.plist"
|
||||||
|
|
||||||
|
echo "Set Libation version number..."
|
||||||
|
sed -i -e "s/VERSION_STRING/$VERSION/" "$BUNDLE_CONTENTS/Info.plist"
|
||||||
|
|
||||||
|
echo "deleting unneeded files.."
|
||||||
|
delfiles=("libmp3lame.x64.so" "ffmpegaac.x64.so" "libation.icns" "Info.plist")
|
||||||
|
for n in "${delfiles[@]}"; do rm "$BUNDLE_MACOS/$n"; done
|
||||||
|
|
||||||
|
echo "Creating app bundle: $BUNDLE-$VERSION.tar.gz"
|
||||||
|
tar -czvf "$BUNDLE-$VERSION.tar.gz" "$BUNDLE"
|
||||||
|
|
||||||
|
mkdir bundle
|
||||||
|
echo "moving to ./bundle/$BUNDLE-$VERSION.tar.gz"
|
||||||
|
mv "$BUNDLE-$VERSION.tar.gz" "./bundle/$BUNDLE-macOS-x64-$VERSION.tgz"
|
||||||
|
|
||||||
|
rm -r "$BUNDLE"
|
||||||
|
|
||||||
|
echo "Done!"
|
||||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
|
using LibationFileManager;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@ -25,9 +26,6 @@ namespace AppScaffolding
|
|||||||
: value;
|
: value;
|
||||||
|
|
||||||
#region appsettings.json
|
#region appsettings.json
|
||||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "appsettings.json");
|
|
||||||
|
|
||||||
public static bool APPSETTINGS_Json_Exists => File.Exists(APPSETTINGS_JSON);
|
|
||||||
|
|
||||||
public static bool APPSETTINGS_TryGet(string key, out string value)
|
public static bool APPSETTINGS_TryGet(string key, out string value)
|
||||||
{
|
{
|
||||||
@ -61,11 +59,7 @@ namespace AppScaffolding
|
|||||||
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
/// <param name="save">True: save if contents changed. False: no not attempt save</param>
|
||||||
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
|
private static void process_APPSETTINGS_Json(Action<JObject> action, bool save = true)
|
||||||
{
|
{
|
||||||
// only insert if not exists
|
var startingContents = File.ReadAllText(Configuration.AppsettingsJsonFile);
|
||||||
if (!APPSETTINGS_Json_Exists)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
|
||||||
|
|
||||||
JObject jObj;
|
JObject jObj;
|
||||||
try
|
try
|
||||||
@ -88,7 +82,7 @@ namespace AppScaffolding
|
|||||||
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
if (startingContents.EqualsInsensitive(endingContents_indented) || startingContents.EqualsInsensitive(endingContents_compact))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
File.WriteAllText(APPSETTINGS_JSON, endingContents_indented);
|
File.WriteAllText(Configuration.AppsettingsJsonFile, endingContents_indented);
|
||||||
System.Threading.Thread.Sleep(100);
|
System.Threading.Thread.Sleep(100);
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace HangoverAvalonia.ViewModels
|
|||||||
|
|
||||||
private void Load_databaseVM()
|
private void Load_databaseVM()
|
||||||
{
|
{
|
||||||
_tab = new(new(() => SqlQuery, s => SqlResults = s, s => SqlResults = s));
|
_tab = new(new DatabaseTabCommands(() => SqlQuery, s => SqlResults += s, s => SqlResults = s));
|
||||||
|
|
||||||
_tab.LoadDatabaseFile();
|
_tab.LoadDatabaseFile();
|
||||||
if (_tab.DbFile is null)
|
if (_tab.DbFile is null)
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net7.0-windows</TargetFramework>
|
<TargetFramework>net7.0-windows</TargetFramework>
|
||||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||||
|
<AssemblyName>Hangover</AssemblyName>
|
||||||
<UseWindowsForms>true</UseWindowsForms>
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
<ApplicationIcon>hangover.ico</ApplicationIcon>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
@ -11,7 +11,6 @@ using System.Threading.Tasks;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Dinah.Core;
|
|
||||||
|
|
||||||
namespace LibationAvalonia
|
namespace LibationAvalonia
|
||||||
{
|
{
|
||||||
@ -53,7 +52,7 @@ namespace LibationAvalonia
|
|||||||
// 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");
|
||||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||||
config.SetLibationFiles(defaultLibationFilesDir);
|
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
@ -86,7 +85,7 @@ namespace LibationAvalonia
|
|||||||
// - error message, Exit()
|
// - error message, Exit()
|
||||||
if (setupDialog.IsNewUser)
|
if (setupDialog.IsNewUser)
|
||||||
{
|
{
|
||||||
setupDialog.Config.SetLibationFiles(Configuration.UserProfile);
|
Configuration.SetLibationFiles(Configuration.UserProfile);
|
||||||
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
ShowSettingsWindow(desktop, setupDialog.Config, OnSettingsCompleted);
|
||||||
}
|
}
|
||||||
else if (setupDialog.IsReturningUser)
|
else if (setupDialog.IsReturningUser)
|
||||||
@ -178,7 +177,7 @@ namespace LibationAvalonia
|
|||||||
|
|
||||||
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
|
private async void OnLibationFilesCompleted(IClassicDesktopStyleApplicationLifetime desktop, LibationFilesDialog libationFilesDialog, Configuration config)
|
||||||
{
|
{
|
||||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
{
|
{
|
||||||
await RunMigrationsAsync(config);
|
await RunMigrationsAsync(config);
|
||||||
|
|||||||
@ -116,20 +116,6 @@
|
|||||||
<ProjectReference Include="..\LibationUiBase\LibationUiBase.csproj" />
|
<ProjectReference Include="..\LibationUiBase\LibationUiBase.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<None Update="glass-with-glow_256.svg">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="Libation.desktop">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
<None Update="ZipExtractor.exe">
|
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
|
||||||
</None>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
|
||||||
<Target Name="SpicNSpan" AfterTargets="Clean">
|
<Target Name="SpicNSpan" AfterTargets="Clean">
|
||||||
<!-- Remove obj folder -->
|
<!-- Remove obj folder -->
|
||||||
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
<RemoveDir Directories="$(BaseIntermediateOutputPath)" />
|
||||||
|
|||||||
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
@ -13,8 +14,30 @@ namespace LibationAvalonia
|
|||||||
{
|
{
|
||||||
static class Program
|
static class Program
|
||||||
{
|
{
|
||||||
static void Main()
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "hangover")
|
||||||
|
{
|
||||||
|
//Launch the Hangover app within the sandbox
|
||||||
|
//We can do this because we're already executing inside the sandbox.
|
||||||
|
//Any process created in the sandbox executes in the same sandbox.
|
||||||
|
//Unfortunately, all sandbox files are read/execute, so no writing!
|
||||||
|
|
||||||
|
Assembly asm = Assembly.GetExecutingAssembly();
|
||||||
|
string path = Path.GetDirectoryName(asm.Location);
|
||||||
|
Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (Configuration.IsMacOs && args?.Length > 0 && args[0] == "cli")
|
||||||
|
{
|
||||||
|
//Open a new Terminal in the sandbox
|
||||||
|
Assembly asm2 = Assembly.GetExecutingAssembly();
|
||||||
|
string libationProgramFiles = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||||
|
Process.Start("/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal", $"\"{libationProgramFiles}\"");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//***********************************************//
|
//***********************************************//
|
||||||
// //
|
// //
|
||||||
// do not use Configuration before this line //
|
// do not use Configuration before this line //
|
||||||
|
|||||||
@ -44,11 +44,11 @@ namespace LibationAvalonia.ViewModels
|
|||||||
private bool _progressBarVisible;
|
private bool _progressBarVisible;
|
||||||
private decimal _speedLimit;
|
private decimal _speedLimit;
|
||||||
|
|
||||||
public int CompletedCount { get => _completedCount; private set { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); } }
|
public int CompletedCount { get => _completedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _completedCount, value); this.RaisePropertyChanged(nameof(AnyCompleted)); }); }
|
||||||
public int QueuedCount { get => _queuedCount; private set { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); } }
|
public int QueuedCount { get => _queuedCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _queuedCount, value); this.RaisePropertyChanged(nameof(AnyQueued)); }); }
|
||||||
public int ErrorCount { get => _errorCount; private set { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); } }
|
public int ErrorCount { get => _errorCount; private set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _errorCount, value); this.RaisePropertyChanged(nameof(AnyErrors)); }); }
|
||||||
public string RunningTime { get => _runningTime; set { this.RaiseAndSetIfChanged(ref _runningTime, value); } }
|
public string RunningTime { get => _runningTime; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _runningTime, value); }); }
|
||||||
public bool ProgressBarVisible { get => _progressBarVisible; set { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); } }
|
public bool ProgressBarVisible { get => _progressBarVisible; set => Dispatcher.UIThread.Post(() => { this.RaiseAndSetIfChanged(ref _progressBarVisible, value); }); }
|
||||||
public bool AnyCompleted => CompletedCount > 0;
|
public bool AnyCompleted => CompletedCount > 0;
|
||||||
public bool AnyQueued => QueuedCount > 0;
|
public bool AnyQueued => QueuedCount > 0;
|
||||||
public bool AnyErrors => ErrorCount > 0;
|
public bool AnyErrors => ErrorCount > 0;
|
||||||
@ -78,8 +78,11 @@ namespace LibationAvalonia.ViewModels
|
|||||||
: _speedLimit > 1 ? 0.1m
|
: _speedLimit > 1 ? 0.1m
|
||||||
: 0.01m;
|
: 0.01m;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
this.RaisePropertyChanged(nameof(SpeedLimitIncrement));
|
||||||
this.RaisePropertyChanged();
|
this.RaisePropertyChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,12 +95,12 @@ namespace LibationAvalonia.ViewModels
|
|||||||
|
|
||||||
ErrorCount = errCount;
|
ErrorCount = errCount;
|
||||||
CompletedCount = completeCount;
|
CompletedCount = completeCount;
|
||||||
this.RaisePropertyChanged(nameof(Progress));
|
Dispatcher.UIThread.Post(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
}
|
}
|
||||||
private void Queue_QueuededCountChanged(object sender, int cueCount)
|
private void Queue_QueuededCountChanged(object sender, int cueCount)
|
||||||
{
|
{
|
||||||
QueuedCount = cueCount;
|
QueuedCount = cueCount;
|
||||||
this.RaisePropertyChanged(nameof(Progress));
|
Dispatcher.UIThread.Post(() => this.RaisePropertyChanged(nameof(Progress)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void WriteLine(string text)
|
public void WriteLine(string text)
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
using System;
|
using LibationFileManager;
|
||||||
using System.Linq;
|
using System;
|
||||||
|
|
||||||
namespace LibationAvalonia.Views
|
namespace LibationAvalonia.Views
|
||||||
{
|
{
|
||||||
@ -16,5 +16,17 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
public async void aboutToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
=> await MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||||
|
|
||||||
|
public void launchHangoverToolStripMenuItem_Click(object sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start("Hangover" + (Configuration.IsWindows ? ".exe" : ""));
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,7 +26,7 @@ namespace LibationAvalonia.Views
|
|||||||
|
|
||||||
//Silently download the update in the background, save it to a temp file.
|
//Silently download the update in the background, save it to a temp file.
|
||||||
|
|
||||||
var zipFile = Path.GetTempFileName();
|
var zipFile = Path.Combine(Path.GetTempPath(), Path.GetFileName(upgradeProperties.ZipUrl));
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
System.Net.Http.HttpClient cli = new();
|
System.Net.Http.HttpClient cli = new();
|
||||||
@ -42,36 +42,6 @@ namespace LibationAvalonia.Views
|
|||||||
return zipFile;
|
return zipFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
void runWindowsUpgrader(string zipFile)
|
|
||||||
{
|
|
||||||
var thisExe = Environment.ProcessPath;
|
|
||||||
var thisDir = Path.GetDirectoryName(thisExe);
|
|
||||||
|
|
||||||
var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
|
|
||||||
|
|
||||||
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
|
||||||
|
|
||||||
var psi = new System.Diagnostics.ProcessStartInfo()
|
|
||||||
{
|
|
||||||
FileName = zipExtractor,
|
|
||||||
UseShellExecute = true,
|
|
||||||
Verb = "runas",
|
|
||||||
WindowStyle = System.Diagnostics.ProcessWindowStyle.Normal,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
ArgumentList =
|
|
||||||
{
|
|
||||||
"--input",
|
|
||||||
zipFile,
|
|
||||||
"--output",
|
|
||||||
thisDir,
|
|
||||||
"--executable",
|
|
||||||
thisExe
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
System.Diagnostics.Process.Start(psi);
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
var upgradeProperties = await Task.Run(LibationScaffolding.GetLatestRelease);
|
||||||
@ -83,26 +53,22 @@ namespace LibationAvalonia.Views
|
|||||||
if (config.GetString(propertyName: ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
|
if (config.GetString(propertyName: ignoreUpdate) == upgradeProperties.LatestRelease.ToString())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, Configuration.IsWindows).ShowDialog<DialogResult>(this);
|
var interop = InteropFactory.Create();
|
||||||
|
|
||||||
|
var notificationResult = await new UpgradeNotificationDialog(upgradeProperties, interop.CanUpdate).ShowDialog<DialogResult>(this);
|
||||||
|
|
||||||
if (notificationResult == DialogResult.Ignore)
|
if (notificationResult == DialogResult.Ignore)
|
||||||
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
config.SetString(upgradeProperties.LatestRelease.ToString(), ignoreUpdate);
|
||||||
|
|
||||||
if (notificationResult != DialogResult.OK || !Configuration.IsWindows) return;
|
if (notificationResult != DialogResult.OK) return;
|
||||||
|
|
||||||
//Download the update file in the background,
|
//Download the update file in the background,
|
||||||
//then wire up installaion on window close.
|
string updateBundle = await downloadUpdate(upgradeProperties);
|
||||||
|
|
||||||
string zipFile = await downloadUpdate(upgradeProperties);
|
if (string.IsNullOrEmpty(updateBundle) || !File.Exists(updateBundle)) return;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(zipFile) || !File.Exists(zipFile))
|
//Install the update
|
||||||
return;
|
interop.InstallUpdate(updateBundle);
|
||||||
|
|
||||||
Closed += (_, _) =>
|
|
||||||
{
|
|
||||||
if (File.Exists(zipFile))
|
|
||||||
runWindowsUpgrader(zipFile);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -131,6 +131,8 @@
|
|||||||
<MenuItem Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
|
<MenuItem Click="accountsToolStripMenuItem_Click" Header="_Accounts..." />
|
||||||
<MenuItem Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
|
<MenuItem Click="basicSettingsToolStripMenuItem_Click" Header="_Settings..." />
|
||||||
<Separator />
|
<Separator />
|
||||||
|
<MenuItem Click="launchHangoverToolStripMenuItem_Click" Header="Launch _Hangover" />
|
||||||
|
<Separator />
|
||||||
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
|
<MenuItem Click="aboutToolStripMenuItem_Click" Header="A_bout..." />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
@ -100,7 +101,7 @@ namespace LibationAvalonia.Views
|
|||||||
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||||
|
|
||||||
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
|
var removeMenuItem = new MenuItem() { Header = "_Remove from library" };
|
||||||
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
|
removeMenuItem.Click += async (_, __) => await Task.Run(() => LibraryCommands.RemoveBook(entry.AudibleProductId));
|
||||||
|
|
||||||
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
var locateFileMenuItem = new MenuItem() { Header = "_Locate file..." };
|
||||||
locateFileMenuItem.Click += async (_, __) =>
|
locateFileMenuItem.Click += async (_, __) =>
|
||||||
|
|||||||
@ -5,12 +5,14 @@ using FileManager;
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
using Dinah.Core.Logging;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
public partial class Configuration
|
public partial class Configuration
|
||||||
{
|
{
|
||||||
private static string APPSETTINGS_JSON { get; } = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "appsettings.json");
|
public static string AppsettingsJsonFile { get; } = getOrCreateAppsettingsFile();
|
||||||
|
|
||||||
private const string LIBATION_FILES_KEY = "LibationFiles";
|
private const string LIBATION_FILES_KEY = "LibationFiles";
|
||||||
|
|
||||||
[Description("Location for storage of program-created files")]
|
[Description("Location for storage of program-created files")]
|
||||||
@ -44,54 +46,93 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
private static string libationFilesPathCache { get; set; }
|
private static string libationFilesPathCache { get; set; }
|
||||||
|
|
||||||
private string getLibationFilesSettingFromJson()
|
/// <summary>
|
||||||
|
/// Try to find appsettings.json in the following locations:
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>
|
||||||
|
/// <description>[App Directory]</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>%LocalAppData%\Libation</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>%AppData%\Libation</description>
|
||||||
|
/// </item>
|
||||||
|
/// <item>
|
||||||
|
/// <description>%Temp%\Libation</description>
|
||||||
|
/// </item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// If not found, try to create it in each of the same locations in-order until successful.
|
||||||
|
///
|
||||||
|
/// <para>This method must complete successfully for Libation to continue.</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>appsettings.json file path</returns>
|
||||||
|
/// <exception cref="ApplicationException">appsettings.json could not be found or created.</exception>
|
||||||
|
private static string getOrCreateAppsettingsFile()
|
||||||
|
{
|
||||||
|
const string appsettings_filename = "appsettings.json";
|
||||||
|
|
||||||
|
//Possible appsettings.json locations, in order of preference.
|
||||||
|
string[] possibleAppsettingsFiles = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), appsettings_filename),
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Libation", appsettings_filename),
|
||||||
|
Path.Combine(UserProfile, appsettings_filename),
|
||||||
|
Path.Combine(Path.GetTempPath(), "Libation", appsettings_filename)
|
||||||
|
};
|
||||||
|
|
||||||
|
//Try to find and validate appsettings.json in each folder
|
||||||
|
foreach (var appsettingsFile in possibleAppsettingsFiles)
|
||||||
|
{
|
||||||
|
if (File.Exists(appsettingsFile))
|
||||||
{
|
{
|
||||||
string startingContents = null;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (File.Exists(APPSETTINGS_JSON))
|
var appSettings = JObject.Parse(File.ReadAllText(appsettingsFile));
|
||||||
{
|
|
||||||
startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
|
||||||
var startingJObj = JObject.Parse(startingContents);
|
|
||||||
|
|
||||||
if (startingJObj.ContainsKey(LIBATION_FILES_KEY))
|
if (appSettings.ContainsKey(LIBATION_FILES_KEY)
|
||||||
{
|
&& appSettings[LIBATION_FILES_KEY] is JValue jval
|
||||||
var startingValue = startingJObj[LIBATION_FILES_KEY].Value<string>();
|
&& jval.Value is string settingsPath
|
||||||
if (!string.IsNullOrWhiteSpace(startingValue))
|
&& !string.IsNullOrWhiteSpace(settingsPath))
|
||||||
return startingValue;
|
return appsettingsFile;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
}
|
||||||
// not found. write to file. read from file
|
|
||||||
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile.ToString() } }.ToString(Formatting.Indented);
|
|
||||||
if (startingContents != endingContents)
|
|
||||||
{
|
|
||||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
|
||||||
System.Threading.Thread.Sleep(100);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Valid appsettings.json not found. Try to create it in each folder.
|
||||||
|
var endingContents = new JObject { { LIBATION_FILES_KEY, UserProfile } }.ToString(Formatting.Indented);
|
||||||
|
foreach (var appsettingsFile in possibleAppsettingsFiles)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(appsettingsFile, endingContents);
|
||||||
|
return appsettingsFile;
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
Log.Logger.TryLogError(ex, $"Failed to create {appsettingsFile}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApplicationException($"Could not locate or create {appsettings_filename}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string getLibationFilesSettingFromJson()
|
||||||
|
{
|
||||||
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
// do not check whether directory exists. special/meta directory (eg: AppDir) is valid
|
||||||
// verify from live file. no try/catch. want failures to be visible
|
// verify from live file. no try/catch. want failures to be visible
|
||||||
var jObjFinal = JObject.Parse(File.ReadAllText(APPSETTINGS_JSON));
|
var jObjFinal = JObject.Parse(File.ReadAllText(AppsettingsJsonFile));
|
||||||
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
var valueFinal = jObjFinal[LIBATION_FILES_KEY].Value<string>();
|
||||||
return valueFinal;
|
return valueFinal;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLibationFiles(string directory)
|
public static void SetLibationFiles(string directory)
|
||||||
{
|
{
|
||||||
// ensure exists
|
|
||||||
if (!File.Exists(APPSETTINGS_JSON))
|
|
||||||
{
|
|
||||||
// getter creates new file, loads PersistentDictionary
|
|
||||||
var _ = LibationFiles;
|
|
||||||
System.Threading.Thread.Sleep(100);
|
|
||||||
}
|
|
||||||
|
|
||||||
libationFilesPathCache = null;
|
libationFilesPathCache = null;
|
||||||
|
|
||||||
var startingContents = File.ReadAllText(APPSETTINGS_JSON);
|
var startingContents = File.ReadAllText(AppsettingsJsonFile);
|
||||||
var jObj = JObject.Parse(startingContents);
|
var jObj = JObject.Parse(startingContents);
|
||||||
|
|
||||||
jObj[LIBATION_FILES_KEY] = directory;
|
jObj[LIBATION_FILES_KEY] = directory;
|
||||||
@ -100,14 +141,17 @@ namespace LibationFileManager
|
|||||||
if (startingContents == endingContents)
|
if (startingContents == endingContents)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// now it's set in the file again but no settings have moved yet
|
|
||||||
File.WriteAllText(APPSETTINGS_JSON, endingContents);
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Log.Logger.Information("Libation files changed {@DebugInfo}", new { APPSETTINGS_JSON, LIBATION_FILES_KEY, directory });
|
// now it's set in the file again but no settings have moved yet
|
||||||
|
File.WriteAllText(AppsettingsJsonFile, endingContents);
|
||||||
|
|
||||||
|
Log.Logger.TryLogInformation("Libation files changed {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||||
|
}
|
||||||
|
catch (IOException ex)
|
||||||
|
{
|
||||||
|
Log.Logger.TryLogError(ex, "Failed to change Libation files location {@DebugInfo}", new { AppsettingsJsonFile, LIBATION_FILES_KEY, directory });
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,7 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
public partial class Configuration : PropertyChangeFilter
|
public partial class Configuration : PropertyChangeFilter
|
||||||
{
|
{
|
||||||
public bool LibationSettingsAreValid
|
public bool LibationSettingsAreValid => SettingsFileIsValid(SettingsFilePath);
|
||||||
=> File.Exists(APPSETTINGS_JSON)
|
|
||||||
&& SettingsFileIsValid(SettingsFilePath);
|
|
||||||
|
|
||||||
public static bool SettingsFileIsValid(string settingsFile)
|
public static bool SettingsFileIsValid(string settingsFile)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
@ -6,6 +7,8 @@ namespace LibationFileManager
|
|||||||
{
|
{
|
||||||
void SetFolderIcon(string image, string directory);
|
void SetFolderIcon(string image, string directory);
|
||||||
void DeleteFolderIcon(string directory);
|
void DeleteFolderIcon(string directory);
|
||||||
void CopyTextToClipboard(string text);
|
Process RunAsRoot(string exe, string args);
|
||||||
|
void InstallUpdate(string updateBundle);
|
||||||
|
bool CanUpdate { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
95
Source/LibationFileManager/NameListFormat.cs
Normal file
95
Source/LibationFileManager/NameListFormat.cs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
using FileManager.NamingTemplate;
|
||||||
|
using NameParser;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace LibationFileManager
|
||||||
|
{
|
||||||
|
internal partial class NameListFormat
|
||||||
|
{
|
||||||
|
public static string Formatter(ITemplateTag _, IEnumerable<string> names, string formatString)
|
||||||
|
{
|
||||||
|
var humanNames = names.Select(n => new HumanName(RemoveSuffix(n), Prefer.FirstOverPrefix));
|
||||||
|
|
||||||
|
var sortedNames = Sort(humanNames, formatString);
|
||||||
|
var nameFormatString = Format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
|
||||||
|
var separatorString = Separator(formatString, defaultValue: ", ");
|
||||||
|
var maxNames = Max(formatString, defaultValue: humanNames.Count());
|
||||||
|
|
||||||
|
var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => FormatName(n, nameFormatString)));
|
||||||
|
|
||||||
|
while (formattedNames.Contains(" "))
|
||||||
|
formattedNames = formattedNames.Replace(" ", " ");
|
||||||
|
|
||||||
|
return formattedNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RemoveSuffix(string namesString)
|
||||||
|
{
|
||||||
|
namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret.");
|
||||||
|
int dashIndex = namesString.IndexOf(" - ");
|
||||||
|
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<HumanName> Sort(IEnumerable<HumanName> humanNames, string formatString)
|
||||||
|
{
|
||||||
|
var sortMatch = SortRegex().Match(formatString);
|
||||||
|
return
|
||||||
|
sortMatch.Success
|
||||||
|
? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First)
|
||||||
|
: sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle)
|
||||||
|
: sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last)
|
||||||
|
: humanNames
|
||||||
|
: humanNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Format(string formatString, string defaultValue)
|
||||||
|
{
|
||||||
|
var formatMatch = FormatRegex().Match(formatString);
|
||||||
|
return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Separator(string formatString, string defaultValue)
|
||||||
|
{
|
||||||
|
var separatorMatch = SeparatorRegex().Match(formatString);
|
||||||
|
return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Max(string formatString, int defaultValue)
|
||||||
|
{
|
||||||
|
var maxMatch = MaxRegex().Match(formatString);
|
||||||
|
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatName(HumanName humanName, string nameFormatString)
|
||||||
|
{
|
||||||
|
//Single-word names parse as first names. Use it as last name.
|
||||||
|
var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last;
|
||||||
|
|
||||||
|
nameFormatString
|
||||||
|
= nameFormatString
|
||||||
|
.Replace("{T}", "{0}")
|
||||||
|
.Replace("{F}", "{1}")
|
||||||
|
.Replace("{M}", "{2}")
|
||||||
|
.Replace("{L}", "{3}")
|
||||||
|
.Replace("{S}", "{4}");
|
||||||
|
|
||||||
|
return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
|
||||||
|
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
|
||||||
|
private static partial Regex SortRegex();
|
||||||
|
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary>
|
||||||
|
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")]
|
||||||
|
private static partial Regex FormatRegex();
|
||||||
|
/// <summary> Separator can be anything </summary>
|
||||||
|
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
|
||||||
|
private static partial Regex SeparatorRegex();
|
||||||
|
/// <summary> Max must have a 1 or 2-digit number </summary>
|
||||||
|
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
|
||||||
|
private static partial Regex MaxRegex();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,14 +1,18 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace LibationFileManager
|
namespace LibationFileManager
|
||||||
{
|
{
|
||||||
public class NullInteropFunctions : IInteropFunctions
|
public class NullInteropFunctions : IInteropFunctions
|
||||||
{
|
{
|
||||||
|
|
||||||
public NullInteropFunctions() { }
|
public NullInteropFunctions() { }
|
||||||
public NullInteropFunctions(params object[] values) { }
|
public NullInteropFunctions(params object[] values) { }
|
||||||
|
|
||||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
public bool CanUpdate => throw new PlatformNotSupportedException();
|
||||||
|
public Process RunAsRoot(string exe, string args) => throw new PlatformNotSupportedException();
|
||||||
|
public void InstallUpdate(string updateBundle) => throw new PlatformNotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using AaxDecrypter;
|
using AaxDecrypter;
|
||||||
using Dinah.Core;
|
using Dinah.Core;
|
||||||
using FileManager;
|
using FileManager;
|
||||||
@ -19,7 +18,7 @@ namespace LibationFileManager
|
|||||||
static abstract IEnumerable<TagCollection> TagCollections { get; }
|
static abstract IEnumerable<TagCollection> TagCollections { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract partial class Templates
|
public abstract class Templates
|
||||||
{
|
{
|
||||||
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
public const string ERROR_FULL_PATH_IS_INVALID = @"No colons or full paths allowed. Eg: should not start with C:\";
|
||||||
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
public const string WARNING_NO_CHAPTER_NUMBER_TAG = "Should include chapter number tag in template used for naming files which are split by chapter. Ie: <ch#> or <ch# 0>";
|
||||||
@ -203,9 +202,9 @@ namespace LibationFileManager
|
|||||||
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
{ TemplateTags.Id, lb => lb.AudibleProductId, v => v },
|
||||||
{ TemplateTags.Title, lb => lb.Title },
|
{ TemplateTags.Title, lb => lb.Title },
|
||||||
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
{ TemplateTags.TitleShort, lb => getTitleShort(lb.Title) },
|
||||||
{ TemplateTags.Author, lb => lb.Authors, NameListFormatter },
|
{ TemplateTags.Author, lb => lb.Authors, NameListFormat.Formatter },
|
||||||
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
{ TemplateTags.FirstAuthor, lb => lb.FirstAuthor },
|
||||||
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormatter },
|
{ TemplateTags.Narrator, lb => lb.Narrators, NameListFormat.Formatter },
|
||||||
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
{ TemplateTags.FirstNarrator, lb => lb.FirstNarrator },
|
||||||
{ TemplateTags.Series, lb => lb.SeriesName },
|
{ TemplateTags.Series, lb => lb.SeriesName },
|
||||||
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
{ TemplateTags.SeriesNumber, lb => lb.SeriesNumber },
|
||||||
@ -252,89 +251,6 @@ namespace LibationFileManager
|
|||||||
|
|
||||||
#region Tag Formatters
|
#region Tag Formatters
|
||||||
|
|
||||||
/// <summary> Sort must have exactly one of the characters F, M, or L </summary>
|
|
||||||
[GeneratedRegex(@"[Ss]ort\(\s*?([FML])\s*?\)")]
|
|
||||||
private static partial Regex NamesSortRegex();
|
|
||||||
/// <summary> Format must have at least one of the string {T}, {F}, {M}, {L}, or {S} </summary>
|
|
||||||
[GeneratedRegex(@"[Ff]ormat\((.*?(?:{[TFMLS]})+.*?)\)")]
|
|
||||||
private static partial Regex NamesFormatRegex();
|
|
||||||
/// <summary> Separator can be anything </summary>
|
|
||||||
[GeneratedRegex(@"[Ss]eparator\((.*?)\)")]
|
|
||||||
private static partial Regex NamesSeparatorRegex();
|
|
||||||
/// <summary> Max must have a 1 or 2-digit number </summary>
|
|
||||||
[GeneratedRegex(@"[Mm]ax\(\s*?(\d{1,2})\s*?\)")]
|
|
||||||
private static partial Regex NamesMaxRegex();
|
|
||||||
|
|
||||||
private static string NameListFormatter(ITemplateTag templateTag, IEnumerable<string> names, string formatString)
|
|
||||||
{
|
|
||||||
var humanNames = names.Select(n => new HumanName(removeSuffix(n), Prefer.FirstOverPrefix));
|
|
||||||
|
|
||||||
var sortedNames = sort(humanNames, formatString);
|
|
||||||
var nameFormatString = format(formatString, defaultValue: "{T} {F} {M} {L} {S}");
|
|
||||||
var separatorString = separator(formatString, defaultValue: ", ");
|
|
||||||
var maxNames = max(formatString, defaultValue: humanNames.Count());
|
|
||||||
|
|
||||||
var formattedNames = string.Join(separatorString, sortedNames.Take(maxNames).Select(n => formatName(n, nameFormatString)));
|
|
||||||
|
|
||||||
while (formattedNames.Contains(" "))
|
|
||||||
formattedNames = formattedNames.Replace(" ", " ");
|
|
||||||
|
|
||||||
return formattedNames;
|
|
||||||
|
|
||||||
static string removeSuffix(string namesString)
|
|
||||||
{
|
|
||||||
namesString = namesString.Replace('’', '\'').Replace(" - Ret.", ", Ret.");
|
|
||||||
int dashIndex = namesString.IndexOf(" - ");
|
|
||||||
return (dashIndex > 0 ? namesString[..dashIndex] : namesString).Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
static IEnumerable<HumanName> sort(IEnumerable<HumanName> humanNames, string formatString)
|
|
||||||
{
|
|
||||||
var sortMatch = NamesSortRegex().Match(formatString);
|
|
||||||
return
|
|
||||||
sortMatch.Success
|
|
||||||
? sortMatch.Groups[1].Value == "F" ? humanNames.OrderBy(n => n.First)
|
|
||||||
: sortMatch.Groups[1].Value == "M" ? humanNames.OrderBy(n => n.Middle)
|
|
||||||
: sortMatch.Groups[1].Value == "L" ? humanNames.OrderBy(n => n.Last)
|
|
||||||
: humanNames
|
|
||||||
: humanNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
static string format(string formatString, string defaultValue)
|
|
||||||
{
|
|
||||||
var formatMatch = NamesFormatRegex().Match(formatString);
|
|
||||||
return formatMatch.Success ? formatMatch.Groups[1].Value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
static string separator(string formatString, string defaultValue)
|
|
||||||
{
|
|
||||||
var separatorMatch = NamesSeparatorRegex().Match(formatString);
|
|
||||||
return separatorMatch.Success ? separatorMatch.Groups[1].Value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int max(string formatString, int defaultValue)
|
|
||||||
{
|
|
||||||
var maxMatch = NamesMaxRegex().Match(formatString);
|
|
||||||
return maxMatch.Success && int.TryParse(maxMatch.Groups[1].Value, out var max) ? int.Max(1, max) : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
static string formatName(HumanName humanName, string nameFormatString)
|
|
||||||
{
|
|
||||||
//Single-word names parse as first names. Use it as last name.
|
|
||||||
var lastName = string.IsNullOrWhiteSpace(humanName.Last) ? humanName.First : humanName.Last;
|
|
||||||
|
|
||||||
nameFormatString
|
|
||||||
= nameFormatString
|
|
||||||
.Replace("{T}", "{0}")
|
|
||||||
.Replace("{F}", "{1}")
|
|
||||||
.Replace("{M}", "{2}")
|
|
||||||
.Replace("{L}", "{3}")
|
|
||||||
.Replace("{S}", "{4}");
|
|
||||||
|
|
||||||
return string.Format(nameFormatString, humanName.Title, humanName.First, humanName.Middle, lastName, humanName.Suffix).Trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string getTitleShort(string title)
|
private static string getTitleShort(string title)
|
||||||
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
=> title?.IndexOf(':') > 0 ? title.Substring(0, title.IndexOf(':')) : title;
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
},
|
},
|
||||||
Configuration.KnownDirectories.UserProfile,
|
Configuration.KnownDirectories.UserProfile,
|
||||||
"Books");
|
"Books");
|
||||||
booksSelectControl.SelectDirectory(config.Books);
|
booksSelectControl.SelectDirectory(config.Books.PathWithoutPrefix);
|
||||||
|
|
||||||
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
saveEpisodesToSeriesFolderCbox.Checked = config.SavePodcastsToParentFolder;
|
||||||
betaOptInCbox.Checked = config.BetaOptIn;
|
betaOptInCbox.Checked = config.BetaOptIn;
|
||||||
|
|||||||
13
Source/LibationWinForms/Form1.Designer.cs
generated
13
Source/LibationWinForms/Form1.Designer.cs
generated
@ -62,6 +62,8 @@
|
|||||||
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.setDownloadedAutoToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
this.toolStripSeparator3 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.removeToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
|
this.toolStripSeparator4 = new System.Windows.Forms.ToolStripSeparator();
|
||||||
|
this.launchHangoverToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.locateAudiobooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.locateAudiobooksToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.settingsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
this.accountsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||||
@ -378,6 +380,8 @@
|
|||||||
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
this.settingsToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||||
this.accountsToolStripMenuItem,
|
this.accountsToolStripMenuItem,
|
||||||
this.basicSettingsToolStripMenuItem,
|
this.basicSettingsToolStripMenuItem,
|
||||||
|
this.toolStripSeparator4,
|
||||||
|
this.launchHangoverToolStripMenuItem,
|
||||||
this.toolStripSeparator2,
|
this.toolStripSeparator2,
|
||||||
this.aboutToolStripMenuItem});
|
this.aboutToolStripMenuItem});
|
||||||
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
this.settingsToolStripMenuItem.Name = "settingsToolStripMenuItem";
|
||||||
@ -572,6 +576,13 @@
|
|||||||
this.locateAudiobooksToolStripMenuItem.Text = "L&ocate Audiobooks";
|
this.locateAudiobooksToolStripMenuItem.Text = "L&ocate Audiobooks";
|
||||||
this.locateAudiobooksToolStripMenuItem.Click += new System.EventHandler(this.locateAudiobooksToolStripMenuItem_Click);
|
this.locateAudiobooksToolStripMenuItem.Click += new System.EventHandler(this.locateAudiobooksToolStripMenuItem_Click);
|
||||||
//
|
//
|
||||||
|
// launchHangoverToolStripMenuItem
|
||||||
|
//
|
||||||
|
this.launchHangoverToolStripMenuItem.Name = "launchHangoverToolStripMenuItem";
|
||||||
|
this.launchHangoverToolStripMenuItem.Size = new System.Drawing.Size(247, 22);
|
||||||
|
this.launchHangoverToolStripMenuItem.Text = "Launch &Hangover";
|
||||||
|
this.launchHangoverToolStripMenuItem.Click += new System.EventHandler(this.launchHangoverToolStripMenuItem_Click);
|
||||||
|
//
|
||||||
// toolStripSeparator3
|
// toolStripSeparator3
|
||||||
//
|
//
|
||||||
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
this.toolStripSeparator3.Name = "toolStripSeparator3";
|
||||||
@ -648,6 +659,8 @@
|
|||||||
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
|
private System.Windows.Forms.ToolStripMenuItem removeToolStripMenuItem;
|
||||||
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator3;
|
||||||
private System.Windows.Forms.ToolStripMenuItem locateAudiobooksToolStripMenuItem;
|
private System.Windows.Forms.ToolStripMenuItem locateAudiobooksToolStripMenuItem;
|
||||||
|
private System.Windows.Forms.ToolStripSeparator toolStripSeparator4;
|
||||||
|
private System.Windows.Forms.ToolStripMenuItem launchHangoverToolStripMenuItem;
|
||||||
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
|
private LibationWinForms.FormattableToolStripMenuItem liberateVisibleToolStripMenuItem_LiberateMenu;
|
||||||
private System.Windows.Forms.SplitContainer splitContainer1;
|
private System.Windows.Forms.SplitContainer splitContainer1;
|
||||||
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
|
private LibationWinForms.ProcessQueue.ProcessQueueControl processBookQueue1;
|
||||||
|
|||||||
@ -14,5 +14,17 @@ namespace LibationWinForms
|
|||||||
|
|
||||||
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
|
private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
=> MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
=> MessageBox.Show($"Libation {AppScaffolding.LibationScaffolding.Variety}{Environment.NewLine}Version {AppScaffolding.LibationScaffolding.BuildVersion}", $"Libation v{AppScaffolding.LibationScaffolding.BuildVersion}");
|
||||||
|
|
||||||
|
private void launchHangoverToolStripMenuItem_Click(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.Diagnostics.Process.Start("Hangover.exe");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Serilog.Log.Logger.Error(ex, "Failed to launch Hangover");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Forms;
|
using System.Windows.Forms;
|
||||||
using ApplicationServices;
|
using ApplicationServices;
|
||||||
using DataLayer;
|
using DataLayer;
|
||||||
@ -122,7 +123,7 @@ namespace LibationWinForms.GridView
|
|||||||
{
|
{
|
||||||
var dgv = (DataGridView)sender;
|
var dgv = (DataGridView)sender;
|
||||||
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
|
var text = dgv[e.ColumnIndex, e.RowIndex].FormattedValue.ToString();
|
||||||
InteropFactory.Create().CopyTextToClipboard(text);
|
Clipboard.SetDataObject(text, false, 5, 150);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
@ -152,7 +153,7 @@ namespace LibationWinForms.GridView
|
|||||||
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
setNotDownloadMenuItem.Click += (_, __) => entry.Book.UpdateBookStatus(LiberatedStatus.NotLiberated);
|
||||||
|
|
||||||
var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
|
var removeMenuItem = new ToolStripMenuItem() { Text = "&Remove from library" };
|
||||||
removeMenuItem.Click += (_, __) => LibraryCommands.RemoveBook(entry.AudibleProductId);
|
removeMenuItem.Click += async (_, __) => await Task.Run(() => LibraryCommands.RemoveBook(entry.AudibleProductId));
|
||||||
|
|
||||||
var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
|
var locateFileMenuItem = new ToolStripMenuItem() { Text = "&Locate file..." };
|
||||||
locateFileMenuItem.Click += (_, __) =>
|
locateFileMenuItem.Click += (_, __) =>
|
||||||
|
|||||||
@ -92,7 +92,7 @@ namespace LibationWinForms
|
|||||||
// 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");
|
||||||
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
if (Configuration.SettingsFileIsValid(defaultSettingsFile))
|
||||||
config.SetLibationFiles(defaultLibationFilesDir);
|
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||||
|
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
return;
|
return;
|
||||||
@ -112,7 +112,7 @@ namespace LibationWinForms
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (setupDialog.IsNewUser)
|
if (setupDialog.IsNewUser)
|
||||||
config.SetLibationFiles(defaultLibationFilesDir);
|
Configuration.SetLibationFiles(defaultLibationFilesDir);
|
||||||
else if (setupDialog.IsReturningUser)
|
else if (setupDialog.IsReturningUser)
|
||||||
{
|
{
|
||||||
var libationFilesDialog = new LibationFilesDialog();
|
var libationFilesDialog = new LibationFilesDialog();
|
||||||
@ -123,7 +123,7 @@ namespace LibationWinForms
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
config.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
Configuration.SetLibationFiles(libationFilesDialog.SelectedDirectory);
|
||||||
if (config.LibationSettingsAreValid)
|
if (config.LibationSettingsAreValid)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@ -34,4 +34,28 @@
|
|||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Properties\Resources.Designer.cs">
|
||||||
|
<DesignTime>True</DesignTime>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Resources.resx</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
<Generator>ResXFileCodeGenerator</Generator>
|
||||||
|
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||||
|
</EmbeddedResource>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="glass-with-glow_256.svg">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="Libation.desktop">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,14 +1,73 @@
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace LinuxConfigApp
|
namespace LinuxConfigApp
|
||||||
{
|
{
|
||||||
internal class LinuxInterop : IInteropFunctions
|
internal class LinuxInterop : IInteropFunctions
|
||||||
{
|
{
|
||||||
|
//Different terminal apps possibly installed on a linux system
|
||||||
|
// [0] console executable
|
||||||
|
// [1] argument to set the concole's title
|
||||||
|
// [2] argument to pass a command to be executed to the terminal
|
||||||
|
static readonly string[][] consoleCommands =
|
||||||
|
{
|
||||||
|
new[] {"konsole", "--title", "-e"},
|
||||||
|
new[] {"gnome-terminal", "--title", "--"},
|
||||||
|
new[] {"mate-terminal", "--title", "-x"},
|
||||||
|
new[] {"xterm", "-T", "-e"},
|
||||||
|
};
|
||||||
|
|
||||||
public LinuxInterop() { }
|
public LinuxInterop() { }
|
||||||
public LinuxInterop(params object[] values) { }
|
public LinuxInterop(params object[] values) { }
|
||||||
|
|
||||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
|
||||||
|
//only run the audo updater is the current app was installed from the
|
||||||
|
//.deb package. Try to detect this by checking if the symlink exists.
|
||||||
|
public bool CanUpdate => Directory.Exists("/usr/bin/libation");
|
||||||
|
public void InstallUpdate(string updateBundle)
|
||||||
|
{
|
||||||
|
RunAsRoot("apt", $"install '{updateBundle}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Process RunAsRoot(string exe, string args)
|
||||||
|
{
|
||||||
|
//cribbed this script from VirtualBox's guest additions installer.
|
||||||
|
//It's designed to launch the system's gui superuser password
|
||||||
|
//prompt across multiple distributions and desktop environments.
|
||||||
|
const string runasroot = "/tmp/runasroot.sh";
|
||||||
|
File.WriteAllBytes(runasroot, Properties.Resources.runasroot);
|
||||||
|
|
||||||
|
string command = $"{exe ?? ""} {args ?? ""}".Trim();
|
||||||
|
|
||||||
|
foreach (var console in consoleCommands)
|
||||||
|
{
|
||||||
|
ProcessStartInfo psi = new()
|
||||||
|
{
|
||||||
|
FileName = console[0],
|
||||||
|
UseShellExecute = false,
|
||||||
|
ArgumentList =
|
||||||
|
{
|
||||||
|
console[1],
|
||||||
|
$"Running '{exe}' as root",
|
||||||
|
console[2],
|
||||||
|
"/bin/sh",
|
||||||
|
runasroot,
|
||||||
|
"Installing libation.deb",
|
||||||
|
command,
|
||||||
|
$"Please run '{command}' manually"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Process.Start(psi);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
73
Source/LoadByOS/LinuxConfigApp/Properties/Resources.Designer.cs
generated
Normal file
73
Source/LoadByOS/LinuxConfigApp/Properties/Resources.Designer.cs
generated
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// <auto-generated>
|
||||||
|
// This code was generated by a tool.
|
||||||
|
// Runtime Version:4.0.30319.42000
|
||||||
|
//
|
||||||
|
// Changes to this file may cause incorrect behavior and will be lost if
|
||||||
|
// the code is regenerated.
|
||||||
|
// </auto-generated>
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
namespace LinuxConfigApp.Properties {
|
||||||
|
using System;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A strongly-typed resource class, for looking up localized strings, etc.
|
||||||
|
/// </summary>
|
||||||
|
// This class was auto-generated by the StronglyTypedResourceBuilder
|
||||||
|
// class via a tool like ResGen or Visual Studio.
|
||||||
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
|
// with the /str option, or rebuild your VS project.
|
||||||
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
|
internal class Resources {
|
||||||
|
|
||||||
|
private static global::System.Resources.ResourceManager resourceMan;
|
||||||
|
|
||||||
|
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||||
|
|
||||||
|
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||||
|
internal Resources() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the cached ResourceManager instance used by this class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||||
|
get {
|
||||||
|
if (object.ReferenceEquals(resourceMan, null)) {
|
||||||
|
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LinuxConfigApp.Properties.Resources", typeof(Resources).Assembly);
|
||||||
|
resourceMan = temp;
|
||||||
|
}
|
||||||
|
return resourceMan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Overrides the current thread's CurrentUICulture property for all
|
||||||
|
/// resource lookups using this strongly typed resource class.
|
||||||
|
/// </summary>
|
||||||
|
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||||
|
internal static global::System.Globalization.CultureInfo Culture {
|
||||||
|
get {
|
||||||
|
return resourceCulture;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
resourceCulture = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Looks up a localized resource of type System.Byte[].
|
||||||
|
/// </summary>
|
||||||
|
internal static byte[] runasroot {
|
||||||
|
get {
|
||||||
|
object obj = ResourceManager.GetObject("runasroot", resourceCulture);
|
||||||
|
return ((byte[])(obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
Source/LoadByOS/LinuxConfigApp/Properties/Resources.resx
Normal file
124
Source/LoadByOS/LinuxConfigApp/Properties/Resources.resx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<root>
|
||||||
|
<!--
|
||||||
|
Microsoft ResX Schema
|
||||||
|
|
||||||
|
Version 2.0
|
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format
|
||||||
|
that is mostly human readable. The generation and parsing of the
|
||||||
|
various data types are done through the TypeConverter classes
|
||||||
|
associated with the data types.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
... ado.net/XML headers & schema ...
|
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||||
|
<resheader name="version">2.0</resheader>
|
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||||
|
</data>
|
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||||
|
<comment>This is a comment</comment>
|
||||||
|
</data>
|
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple
|
||||||
|
name/value pairs.
|
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a
|
||||||
|
type or mimetype. Type corresponds to a .NET class that support
|
||||||
|
text/value conversion through the TypeConverter architecture.
|
||||||
|
Classes that don't support this are serialized and stored with the
|
||||||
|
mimetype set.
|
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the
|
||||||
|
ResXResourceReader how to depersist the object. This is currently not
|
||||||
|
extensible. For a given mimetype the value must be set accordingly:
|
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||||
|
that the ResXResourceWriter will generate, however the reader can
|
||||||
|
read any of the formats listed below.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64
|
||||||
|
value : The object must be serialized with
|
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||||
|
value : The object must be serialized into a byte array
|
||||||
|
: using a System.ComponentModel.TypeConverter
|
||||||
|
: and then encoded with base64 encoding.
|
||||||
|
-->
|
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||||
|
<xsd:element name="root" msdata:IsDataSet="true">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:choice maxOccurs="unbounded">
|
||||||
|
<xsd:element name="metadata">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="assembly">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:attribute name="alias" type="xsd:string" />
|
||||||
|
<xsd:attribute name="name" type="xsd:string" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="data">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||||
|
<xsd:attribute ref="xml:space" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
<xsd:element name="resheader">
|
||||||
|
<xsd:complexType>
|
||||||
|
<xsd:sequence>
|
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||||
|
</xsd:sequence>
|
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:choice>
|
||||||
|
</xsd:complexType>
|
||||||
|
</xsd:element>
|
||||||
|
</xsd:schema>
|
||||||
|
<resheader name="resmimetype">
|
||||||
|
<value>text/microsoft-resx</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="version">
|
||||||
|
<value>2.0</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="reader">
|
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<resheader name="writer">
|
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</resheader>
|
||||||
|
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
|
||||||
|
<data name="runasroot" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
|
<value>..\Resources\runasroot.sh;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||||
|
</data>
|
||||||
|
</root>
|
||||||
188
Source/LoadByOS/LinuxConfigApp/Resources/runasroot.sh
Normal file
188
Source/LoadByOS/LinuxConfigApp/Resources/runasroot.sh
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# $Id: runasroot.sh 153224 2022-08-22 17:43:14Z klaus $
|
||||||
|
## @file
|
||||||
|
# VirtualBox privileged execution helper script for Linux and Solaris
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright (C) 2009-2022 Oracle and/or its affiliates.
|
||||||
|
#
|
||||||
|
# This file is part of VirtualBox base platform packages, as
|
||||||
|
# available from https://www.virtualbox.org.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or
|
||||||
|
# modify it under the terms of the GNU General Public License
|
||||||
|
# as published by the Free Software Foundation, in version 3 of the
|
||||||
|
# License.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
# General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, see <https://www.gnu.org/licenses>.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
#
|
||||||
|
|
||||||
|
# Deal with differing "which" semantics
|
||||||
|
mywhich() {
|
||||||
|
which "$1" 2>/dev/null | grep -v "no $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the name and execute switch for a useful terminal emulator
|
||||||
|
#
|
||||||
|
# Sets $gxtpath to the emulator path or empty
|
||||||
|
# Sets $gxttitle to the "title" switch for that emulator
|
||||||
|
# Sets $gxtexec to the "execute" switch for that emulator
|
||||||
|
# May clobber $gtx*
|
||||||
|
# Calls mywhich
|
||||||
|
getxterm() {
|
||||||
|
# gnome-terminal uses -e differently to other emulators
|
||||||
|
for gxti in "konsole --title -e" "gnome-terminal --title -x" "xterm -T -e"; do
|
||||||
|
set $gxti
|
||||||
|
gxtpath="`mywhich $1`"
|
||||||
|
case "$gxtpath" in ?*)
|
||||||
|
gxttitle=$2
|
||||||
|
gxtexec=$3
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Quotes its argument by inserting '\' in front of every character save
|
||||||
|
# for 'A-Za-z0-9/'. Prints the result to stdout.
|
||||||
|
quotify() {
|
||||||
|
echo "$1" | sed -e 's/\([^a-zA-Z0-9/]\)/\\\1/g'
|
||||||
|
}
|
||||||
|
|
||||||
|
ostype=`uname -s`
|
||||||
|
if test "$ostype" != "Linux" && test "$ostype" != "SunOS" ; then
|
||||||
|
echo "Linux/Solaris not detected."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
HAS_TERMINAL=""
|
||||||
|
case "$1" in "--has-terminal")
|
||||||
|
shift
|
||||||
|
HAS_TERMINAL="yes"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$#" in "2"|"3")
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: `basename $0` DESCRIPTION COMMAND [ADVICE]" >&2
|
||||||
|
echo >&2
|
||||||
|
echo "Attempt to execute COMMAND with root privileges, displaying DESCRIPTION if" >&2
|
||||||
|
echo "possible and displaying ADVICE if possible if no su(1)-like tool is available." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
DESCRIPTION=$1
|
||||||
|
COMMAND=$2
|
||||||
|
ADVICE=$3
|
||||||
|
PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin:/usr/local/sbin:/usr/X11/bin
|
||||||
|
|
||||||
|
case "$ostype" in SunOS)
|
||||||
|
PATH=$PATH:/usr/sfw/bin:/usr/gnu/bin:/usr/xpg4/bin:/usr/xpg6/bin:/usr/openwin/bin:/usr/ucb
|
||||||
|
GKSU_SWITCHES="-au root"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
GKSU_SWITCHES=""
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$HAS_TERMINAL" in "")
|
||||||
|
case "$DISPLAY" in ?*)
|
||||||
|
KDESUDO="`mywhich kdesudo`"
|
||||||
|
case "$KDESUDO" in ?*)
|
||||||
|
eval "`quotify "$KDESUDO"` --comment `quotify "$DESCRIPTION"` -- $COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
KDESU="`mywhich kdesu`"
|
||||||
|
case "$KDESU" in ?*)
|
||||||
|
"$KDESU" -c "$COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
GKSU="`mywhich gksu`"
|
||||||
|
case "$GKSU" in ?*)
|
||||||
|
# Older gksu does not grok --description nor '--' and multiple args.
|
||||||
|
# @todo which versions do?
|
||||||
|
# "$GKSU" --description "$DESCRIPTION" -- "$@"
|
||||||
|
# Note that $GKSU_SWITCHES is NOT quoted in the following
|
||||||
|
"$GKSU" $GKSU_SWITCHES "$COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac # $DISPLAY
|
||||||
|
;;
|
||||||
|
esac # ! $HAS_TERMINAL
|
||||||
|
|
||||||
|
# pkexec may work for ssh console sessions as well if the right agents
|
||||||
|
# are installed. However it is very generic and does not allow for any
|
||||||
|
# custom messages. Thus it comes after gksu.
|
||||||
|
## @todo should we insist on either a display or a terminal?
|
||||||
|
# case "$DISPLAY$HAS_TERMINAL" in ?*)
|
||||||
|
PKEXEC="`mywhich pkexec`"
|
||||||
|
case "$PKEXEC" in ?*)
|
||||||
|
eval "\"$PKEXEC\" $COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# ;;S
|
||||||
|
#esac
|
||||||
|
|
||||||
|
case "$HAS_TERMINAL" in ?*)
|
||||||
|
USE_SUDO=
|
||||||
|
grep -q Ubuntu /etc/lsb-release 2>/dev/null && USE_SUDO=true
|
||||||
|
# On Ubuntu we need sudo instead of su. Assume this works, and is only
|
||||||
|
# needed for Ubuntu until proven wrong.
|
||||||
|
case $USE_SUDO in true)
|
||||||
|
SUDO_COMMAND="`quotify "$SUDO"` -- $COMMAND"
|
||||||
|
eval "$SUDO_COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
SU="`mywhich su`"
|
||||||
|
case "$SU" in ?*)
|
||||||
|
"$SU" - root -c "$COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# The ultimate fallback is running 'su -' within an xterm. We use the
|
||||||
|
# title of the xterm to tell what is going on.
|
||||||
|
case "$DISPLAY" in ?*)
|
||||||
|
SU="`mywhich su`"
|
||||||
|
case "$SU" in ?*)
|
||||||
|
getxterm
|
||||||
|
case "$gxtpath" in ?*)
|
||||||
|
"$gxtpath" "$gxttitle" "$DESCRIPTION - su" "$gxtexec" su - root -c "$COMMAND"
|
||||||
|
exit
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
esac
|
||||||
|
esac # $DISPLAY
|
||||||
|
|
||||||
|
# Failure...
|
||||||
|
case "$DISPLAY" in ?*)
|
||||||
|
echo "Unable to locate 'pkexec', 'gksu' or 'su+xterm'. $ADVICE" >&2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unable to locate 'pkexec'. $ADVICE" >&2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
31
Source/LoadByOS/MacOSConfigApp/Info.plist
Normal file
31
Source/LoadByOS/MacOSConfigApp/Info.plist
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>Libation</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>Libation</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.libation.macos</string>
|
||||||
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<key>CFBundleIconFile</key>
|
||||||
|
<string>libation.icns</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>VERSION_STRING</string>
|
||||||
|
<key>com.apple.security.app-sandbox</key>
|
||||||
|
<false/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-executable-page-protection</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.automation.apple-events</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -34,4 +34,13 @@
|
|||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Info.plist">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Update="libation.icns">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@ -1,14 +1,78 @@
|
|||||||
using LibationFileManager;
|
using LibationFileManager;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace MacOSConfigApp
|
namespace MacOSConfigApp
|
||||||
{
|
{
|
||||||
internal class MacOSInterop : IInteropFunctions
|
internal class MacOSInterop : IInteropFunctions
|
||||||
{
|
{
|
||||||
|
private const string AppPath = "/Applications/Libation.app";
|
||||||
public MacOSInterop() { }
|
public MacOSInterop() { }
|
||||||
public MacOSInterop(params object[] values) { }
|
public MacOSInterop(params object[] values) { }
|
||||||
|
|
||||||
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
public void SetFolderIcon(string image, string directory) => throw new PlatformNotSupportedException();
|
||||||
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
public void DeleteFolderIcon(string directory) => throw new PlatformNotSupportedException();
|
||||||
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
|
|
||||||
|
//I haven't figured out how to find the app bundle's directory from within
|
||||||
|
//the running process, so don't update unless it's "installed" in /Applications
|
||||||
|
public bool CanUpdate => Directory.Exists(AppPath);
|
||||||
|
|
||||||
|
public void InstallUpdate(string updateBundle)
|
||||||
|
{
|
||||||
|
Serilog.Log.Information($"Extracting update bundle to {AppPath}");
|
||||||
|
|
||||||
|
//tar wil overwrite existing without elevated privileges
|
||||||
|
Process.Start("tar", $"-xzf \"{updateBundle}\" -C \"/Applications\"").WaitForExit();
|
||||||
|
|
||||||
|
//For now, it seems like this step is unnecessary. We can overwrite and
|
||||||
|
//run Libation without needing to re-add the exception. This is insurance.
|
||||||
|
RunAsRoot(null, $"""
|
||||||
|
sudo spctl --master-disable
|
||||||
|
sudo spctl --add --label 'Libation' {AppPath}
|
||||||
|
open {AppPath}
|
||||||
|
sudo spctl --master-enable
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
//Using osascript -e '[script]' works from the terminal, but I haven't figured
|
||||||
|
//out the syntax for it to work from create_process, so write to stdin instead.
|
||||||
|
public Process RunAsRoot(string _, string command)
|
||||||
|
{
|
||||||
|
const string osascript = "osascript";
|
||||||
|
var fullCommand = $"do shell script \"{command}\" with administrator privileges";
|
||||||
|
|
||||||
|
var psi = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = osascript,
|
||||||
|
UseShellExecute = false,
|
||||||
|
Arguments = "-",
|
||||||
|
RedirectStandardError= true,
|
||||||
|
RedirectStandardOutput= true,
|
||||||
|
RedirectStandardInput= true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Serilog.Log.Logger.Information($"running {osascript} as root: {{script}}", fullCommand);
|
||||||
|
|
||||||
|
var proc = Process.Start(psi);
|
||||||
|
proc.ErrorDataReceived += Proc_ErrorDataReceived;
|
||||||
|
proc.OutputDataReceived += Proc_OutputDataReceived;
|
||||||
|
proc.BeginErrorReadLine();
|
||||||
|
proc.BeginOutputReadLine();
|
||||||
|
proc.StandardInput.WriteLine(fullCommand);
|
||||||
|
proc.StandardInput.Close();
|
||||||
|
|
||||||
|
return proc;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data != null)
|
||||||
|
Serilog.Log.Logger.Information("stderr: {data}", e.Data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Proc_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data!= null)
|
||||||
|
Serilog.Log.Logger.Information("stderr: {data}", e.Data);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
Source/LoadByOS/MacOSConfigApp/libation.icns
Normal file
BIN
Source/LoadByOS/MacOSConfigApp/libation.icns
Normal file
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -35,8 +36,31 @@ namespace WindowsConfigApp
|
|||||||
|
|
||||||
public void DeleteFolderIcon(string directory)
|
public void DeleteFolderIcon(string directory)
|
||||||
=> new DirectoryInfo(directory)?.DeleteIcon();
|
=> new DirectoryInfo(directory)?.DeleteIcon();
|
||||||
|
public bool CanUpdate => true;
|
||||||
|
public void InstallUpdate(string updateBundle)
|
||||||
|
{
|
||||||
|
var thisExe = Environment.ProcessPath;
|
||||||
|
var thisDir = Path.GetDirectoryName(thisExe);
|
||||||
|
var zipExtractor = Path.Combine(Path.GetTempPath(), "ZipExtractor.exe");
|
||||||
|
|
||||||
public void CopyTextToClipboard(string text)
|
File.Copy("ZipExtractor.exe", zipExtractor, overwrite: true);
|
||||||
=> Clipboard.SetText(text);
|
|
||||||
|
RunAsRoot(zipExtractor, $"--input \"{updateBundle}\" --output \"{thisDir}\" --executable \"{thisExe}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Process RunAsRoot(string exe, string args)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo()
|
||||||
|
{
|
||||||
|
FileName = exe,
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas",
|
||||||
|
WindowStyle = ProcessWindowStyle.Normal,
|
||||||
|
CreateNoWindow = true,
|
||||||
|
Arguments = args
|
||||||
|
};
|
||||||
|
|
||||||
|
return Process.Start(psi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,4 +40,10 @@
|
|||||||
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="ZipExtractor.exe">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
Loading…
x
Reference in New Issue
Block a user