New feature #241 : Auto download episodes after scanning. Setting is on Import Library tab
This commit is contained in:
parent
99527453a7
commit
f7a482659c
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0-windows</TargetFramework>
|
<TargetFramework>net6.0-windows</TargetFramework>
|
||||||
<Version>7.8.1.1</Version>
|
<Version>7.9.0.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@ -138,6 +138,9 @@ namespace AppScaffolding
|
|||||||
|
|
||||||
if (!config.Exists(nameof(config.DownloadCoverArt)))
|
if (!config.Exists(nameof(config.DownloadCoverArt)))
|
||||||
config.DownloadCoverArt = true;
|
config.DownloadCoverArt = true;
|
||||||
|
|
||||||
|
if (!config.Exists(nameof(config.AutoDownloadEpisodes)))
|
||||||
|
config.AutoDownloadEpisodes = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
/// <summary>Initialize logging. Wire-up events. Run after migration</summary>
|
||||||
|
|||||||
@ -341,7 +341,14 @@ namespace ApplicationServices
|
|||||||
|
|
||||||
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
// below are queries, not commands. maybe I should make a LibraryQueries. except there's already one of those...
|
||||||
|
|
||||||
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded) { }
|
public record LibraryStats(int booksFullyBackedUp, int booksDownloadedOnly, int booksNoProgress, int booksError, int pdfsDownloaded, int pdfsNotDownloaded)
|
||||||
|
{
|
||||||
|
public int PendingBooks => booksNoProgress + booksDownloadedOnly;
|
||||||
|
public bool HasPendingBooks => PendingBooks > 0;
|
||||||
|
|
||||||
|
public bool HasBookResults => 0 < (booksFullyBackedUp + booksDownloadedOnly + booksNoProgress + booksError);
|
||||||
|
public bool HasPdfResults => 0 < (pdfsNotDownloaded + pdfsDownloaded);
|
||||||
|
}
|
||||||
public static LibraryStats GetCounts()
|
public static LibraryStats GetCounts()
|
||||||
{
|
{
|
||||||
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
var libraryBooks = DbContexts.GetLibrary_Flat_NoTracking();
|
||||||
|
|||||||
@ -268,6 +268,13 @@ namespace LibationFileManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Description("Auto download episodes? Efter scan, download new books in 'checked' accounts.")]
|
||||||
|
public bool AutoDownloadEpisodes
|
||||||
|
{
|
||||||
|
get => persistentDictionary.GetNonString<bool>(nameof(AutoDownloadEpisodes));
|
||||||
|
set => persistentDictionary.SetNonString(nameof(AutoDownloadEpisodes), value);
|
||||||
|
}
|
||||||
|
|
||||||
#region templates: custom file naming
|
#region templates: custom file naming
|
||||||
|
|
||||||
[Description("How to format the folders in which files will be saved")]
|
[Description("How to format the folders in which files will be saved")]
|
||||||
|
|||||||
1927
Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
1927
Source/LibationWinForms/Dialogs/SettingsDialog.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@ -35,6 +35,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
this.showImportedStatsCb.Text = desc(nameof(config.ShowImportedStats));
|
||||||
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
this.importEpisodesCb.Text = desc(nameof(config.ImportEpisodes));
|
||||||
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
this.downloadEpisodesCb.Text = desc(nameof(config.DownloadEpisodes));
|
||||||
|
this.autoDownloadEpisodesCb.Text = desc(nameof(config.AutoDownloadEpisodes));
|
||||||
|
|
||||||
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
this.booksLocationDescLbl.Text = desc(nameof(config.Books));
|
||||||
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
this.inProgressDescLbl.Text = desc(nameof(config.InProgress));
|
||||||
@ -80,6 +81,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
showImportedStatsCb.Checked = config.ShowImportedStats;
|
showImportedStatsCb.Checked = config.ShowImportedStats;
|
||||||
importEpisodesCb.Checked = config.ImportEpisodes;
|
importEpisodesCb.Checked = config.ImportEpisodes;
|
||||||
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
downloadEpisodesCb.Checked = config.DownloadEpisodes;
|
||||||
|
autoDownloadEpisodesCb.Checked = config.AutoDownloadEpisodes;
|
||||||
|
|
||||||
lameTargetRb_CheckedChanged(this, e);
|
lameTargetRb_CheckedChanged(this, e);
|
||||||
LameMatchSourceBRCbox_CheckedChanged(this, e);
|
LameMatchSourceBRCbox_CheckedChanged(this, e);
|
||||||
@ -204,6 +206,7 @@ namespace LibationWinForms.Dialogs
|
|||||||
config.ShowImportedStats = showImportedStatsCb.Checked;
|
config.ShowImportedStats = showImportedStatsCb.Checked;
|
||||||
config.ImportEpisodes = importEpisodesCb.Checked;
|
config.ImportEpisodes = importEpisodesCb.Checked;
|
||||||
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
config.DownloadEpisodes = downloadEpisodesCb.Checked;
|
||||||
|
config.AutoDownloadEpisodes = autoDownloadEpisodesCb.Checked;
|
||||||
|
|
||||||
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
config.InProgress = inProgressSelectControl.SelectedDirectory;
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ namespace LibationWinForms
|
|||||||
{
|
{
|
||||||
public partial class Form1
|
public partial class Form1
|
||||||
{
|
{
|
||||||
|
private System.ComponentModel.BackgroundWorker updateCountsBw = new();
|
||||||
|
|
||||||
protected void Configure_BackupCounts()
|
protected void Configure_BackupCounts()
|
||||||
{
|
{
|
||||||
// init formattable
|
// init formattable
|
||||||
@ -16,22 +18,23 @@ namespace LibationWinForms
|
|||||||
Load += setBackupCounts;
|
Load += setBackupCounts;
|
||||||
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
LibraryCommands.LibrarySizeChanged += setBackupCounts;
|
||||||
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
LibraryCommands.BookUserDefinedItemCommitted += setBackupCounts;
|
||||||
}
|
|
||||||
|
|
||||||
private System.ComponentModel.BackgroundWorker updateCountsBw;
|
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
||||||
|
updateCountsBw.RunWorkerCompleted += exportMenuEnable;
|
||||||
|
updateCountsBw.RunWorkerCompleted += updateBottomBookNumbers;
|
||||||
|
updateCountsBw.RunWorkerCompleted += update_BeginBookBackups_menuItem;
|
||||||
|
updateCountsBw.RunWorkerCompleted += updateBottomPdfNumbers;
|
||||||
|
updateCountsBw.RunWorkerCompleted += udpate_BeginPdfOnlyBackups_menuItem;
|
||||||
|
}
|
||||||
|
|
||||||
private bool runBackupCountsAgain;
|
private bool runBackupCountsAgain;
|
||||||
|
|
||||||
private void setBackupCounts(object _, object __)
|
private void setBackupCounts(object _, object __)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = true;
|
runBackupCountsAgain = true;
|
||||||
|
|
||||||
if (updateCountsBw is not null)
|
if (!updateCountsBw.IsBusy)
|
||||||
return;
|
updateCountsBw.RunWorkerAsync();
|
||||||
|
|
||||||
updateCountsBw = new System.ComponentModel.BackgroundWorker();
|
|
||||||
updateCountsBw.DoWork += UpdateCountsBw_DoWork;
|
|
||||||
updateCountsBw.RunWorkerCompleted += UpdateCountsBw_RunWorkerCompleted;
|
|
||||||
updateCountsBw.RunWorkerAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
private void UpdateCountsBw_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
|
||||||
@ -39,87 +42,78 @@ namespace LibationWinForms
|
|||||||
while (runBackupCountsAgain)
|
while (runBackupCountsAgain)
|
||||||
{
|
{
|
||||||
runBackupCountsAgain = false;
|
runBackupCountsAgain = false;
|
||||||
|
e.Result = LibraryCommands.GetCounts();
|
||||||
var libraryStats = LibraryCommands.GetCounts();
|
|
||||||
e.Result = libraryStats;
|
|
||||||
}
|
}
|
||||||
updateCountsBw = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCountsBw_RunWorkerCompleted(object sender, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
private void exportMenuEnable(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||||
{
|
{
|
||||||
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
|
exportLibraryToolStripMenuItem.Enabled = libraryStats.HasBookResults;
|
||||||
setBookBackupCounts(libraryStats);
|
|
||||||
setPdfBackupCounts(libraryStats);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
|
// this cannot be cleanly be FormattableToolStripMenuItem because of the optional "Errors" text
|
||||||
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
|
private const string backupsCountsLbl_Format = "BACKUPS: No progress: {0} In process: {1} Fully backed up: {2}";
|
||||||
|
|
||||||
private void setBookBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
private void updateBottomBookNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||||
{
|
{
|
||||||
var pending = libraryStats.booksNoProgress + libraryStats.booksDownloadedOnly;
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
var hasResults = 0 < (libraryStats.booksFullyBackedUp + libraryStats.booksDownloadedOnly + libraryStats.booksNoProgress + libraryStats.booksError);
|
|
||||||
|
|
||||||
// enable/disable export
|
var formatString
|
||||||
{
|
= !libraryStats.HasBookResults ? "No books. Begin by importing your library"
|
||||||
exportLibraryToolStripMenuItem.Enabled = hasResults;
|
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
|
||||||
}
|
: libraryStats.HasPendingBooks ? backupsCountsLbl_Format
|
||||||
|
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
|
||||||
// update bottom numbers
|
var statusStripText = string.Format(formatString,
|
||||||
{
|
libraryStats.booksNoProgress,
|
||||||
var formatString
|
libraryStats.booksDownloadedOnly,
|
||||||
= !hasResults ? "No books. Begin by importing your library"
|
libraryStats.booksFullyBackedUp,
|
||||||
: libraryStats.booksError > 0 ? backupsCountsLbl_Format + " Errors: {3}"
|
libraryStats.booksError);
|
||||||
: pending > 0 ? backupsCountsLbl_Format
|
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
|
||||||
: $"All {"book".PluralizeWithCount(libraryStats.booksFullyBackedUp)} backed up";
|
|
||||||
var statusStripText = string.Format(formatString,
|
|
||||||
libraryStats.booksNoProgress,
|
|
||||||
libraryStats.booksDownloadedOnly,
|
|
||||||
libraryStats.booksFullyBackedUp,
|
|
||||||
libraryStats.booksError);
|
|
||||||
statusStrip1.UIThreadAsync(() => backupsCountsLbl.Text = statusStripText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update 'begin book backups' menu item
|
|
||||||
{
|
|
||||||
var menuItemText
|
|
||||||
= pending > 0
|
|
||||||
? $"{pending} remaining"
|
|
||||||
: "All books have been liberated";
|
|
||||||
menuStrip1.UIThreadAsync(() =>
|
|
||||||
{
|
|
||||||
beginBookBackupsToolStripMenuItem.Format(menuItemText);
|
|
||||||
beginBookBackupsToolStripMenuItem.Enabled = pending > 0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
private void setPdfBackupCounts(LibraryCommands.LibraryStats libraryStats)
|
|
||||||
{
|
|
||||||
// update bottom numbers
|
|
||||||
{
|
|
||||||
var hasResults = 0 < (libraryStats.pdfsNotDownloaded + libraryStats.pdfsDownloaded);
|
|
||||||
// don't need to assign the output of Format(). It just makes this logic cleaner
|
|
||||||
var statusStripText
|
|
||||||
= !hasResults ? ""
|
|
||||||
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
|
|
||||||
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
|
|
||||||
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update 'begin pdf only backups' menu item
|
// update 'begin book backups' menu item
|
||||||
|
private void update_BeginBookBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||||
|
{
|
||||||
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
|
|
||||||
|
var menuItemText
|
||||||
|
= libraryStats.HasPendingBooks
|
||||||
|
? $"{libraryStats.PendingBooks} remaining"
|
||||||
|
: "All books have been liberated";
|
||||||
|
menuStrip1.UIThreadAsync(() =>
|
||||||
{
|
{
|
||||||
var menuItemText
|
beginBookBackupsToolStripMenuItem.Format(menuItemText);
|
||||||
= libraryStats.pdfsNotDownloaded > 0
|
beginBookBackupsToolStripMenuItem.Enabled = libraryStats.HasPendingBooks;
|
||||||
? $"{libraryStats.pdfsNotDownloaded} remaining"
|
});
|
||||||
: "All PDFs have been downloaded";
|
}
|
||||||
menuStrip1.UIThreadAsync(() =>
|
|
||||||
{
|
private void updateBottomPdfNumbers(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||||
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
|
{
|
||||||
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
});
|
|
||||||
}
|
// don't need to assign the output of Format(). It just makes this logic cleaner
|
||||||
|
var statusStripText
|
||||||
|
= !libraryStats.HasPdfResults ? ""
|
||||||
|
: libraryStats.pdfsNotDownloaded > 0 ? pdfsCountsLbl.Format(libraryStats.pdfsNotDownloaded, libraryStats.pdfsDownloaded)
|
||||||
|
: $"| All {libraryStats.pdfsDownloaded} PDFs downloaded";
|
||||||
|
statusStrip1.UIThreadAsync(() => pdfsCountsLbl.Text = statusStripText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update 'begin pdf only backups' menu item
|
||||||
|
private void udpate_BeginPdfOnlyBackups_menuItem(object _, System.ComponentModel.RunWorkerCompletedEventArgs e)
|
||||||
|
{
|
||||||
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
|
|
||||||
|
var menuItemText
|
||||||
|
= libraryStats.pdfsNotDownloaded > 0
|
||||||
|
? $"{libraryStats.pdfsNotDownloaded} remaining"
|
||||||
|
: "All PDFs have been downloaded";
|
||||||
|
menuStrip1.UIThreadAsync(() =>
|
||||||
|
{
|
||||||
|
beginPdfBackupsToolStripMenuItem.Format(menuItemText);
|
||||||
|
beginPdfBackupsToolStripMenuItem.Enabled = libraryStats.pdfsNotDownloaded > 0;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ namespace LibationWinForms
|
|||||||
private void Configure_Liberate() { }
|
private void Configure_Liberate() { }
|
||||||
|
|
||||||
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
|
//GetLibrary_Flat_NoTracking() may take a long time on a hugh library. so run in new thread
|
||||||
private async void beginBookBackupsToolStripMenuItem_Click(object sender, EventArgs e)
|
private async void beginBookBackupsToolStripMenuItem_Click(object _ = null, EventArgs __ = null)
|
||||||
{
|
{
|
||||||
SetQueueCollapseState(false);
|
SetQueueCollapseState(false);
|
||||||
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
|
await Task.Run(() => processBookQueue1.AddDownloadDecrypt(ApplicationServices.DbContexts.GetLibrary_Flat_NoTracking()
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
using Dinah.Core.Drawing;
|
|
||||||
using LibationFileManager;
|
|
||||||
|
|
||||||
namespace LibationWinForms
|
|
||||||
{
|
|
||||||
public partial class Form1
|
|
||||||
{
|
|
||||||
private void Configure_PictureStorage()
|
|
||||||
{
|
|
||||||
// init default/placeholder cover art
|
|
||||||
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
|
||||||
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
37
Source/LibationWinForms/Form1._NonUI.cs
Normal file
37
Source/LibationWinForms/Form1._NonUI.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ApplicationServices;
|
||||||
|
using Dinah.Core.Drawing;
|
||||||
|
using LibationFileManager;
|
||||||
|
|
||||||
|
namespace LibationWinForms
|
||||||
|
{
|
||||||
|
public partial class Form1
|
||||||
|
{
|
||||||
|
private void Configure_NonUI()
|
||||||
|
{
|
||||||
|
// init default/placeholder cover art
|
||||||
|
var format = System.Drawing.Imaging.ImageFormat.Jpeg;
|
||||||
|
PictureStorage.SetDefaultImage(PictureSize._80x80, Properties.Resources.default_cover_80x80.ToBytes(format));
|
||||||
|
PictureStorage.SetDefaultImage(PictureSize._300x300, Properties.Resources.default_cover_300x300.ToBytes(format));
|
||||||
|
PictureStorage.SetDefaultImage(PictureSize._500x500, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||||
|
PictureStorage.SetDefaultImage(PictureSize.Native, Properties.Resources.default_cover_500x500.ToBytes(format));
|
||||||
|
|
||||||
|
// wire-up event to automatically download after scan.
|
||||||
|
// winforms only. this should NOT be allowed in cli
|
||||||
|
updateCountsBw.RunWorkerCompleted += (object sender, System.ComponentModel.RunWorkerCompletedEventArgs e) =>
|
||||||
|
{
|
||||||
|
if (!Configuration.Instance.AutoDownloadEpisodes)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var libraryStats = e.Result as LibraryCommands.LibraryStats;
|
||||||
|
|
||||||
|
if ((libraryStats.booksNoProgress + libraryStats.pdfsNotDownloaded) > 0)
|
||||||
|
beginBookBackupsToolStripMenuItem_Click();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,7 +38,6 @@ namespace LibationWinForms
|
|||||||
// these should do nothing interesting yet (storing simple var, subscribe to events) and should never rely on each other for order.
|
// these should do nothing interesting yet (storing simple var, subscribe to events) and should never rely on each other for order.
|
||||||
// otherwise, order could be an issue.
|
// otherwise, order could be an issue.
|
||||||
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
|
// eg: if one of these init'd productsGrid, then another can't reliably subscribe to it
|
||||||
Configure_PictureStorage();
|
|
||||||
Configure_BackupCounts();
|
Configure_BackupCounts();
|
||||||
Configure_ScanAuto();
|
Configure_ScanAuto();
|
||||||
Configure_ScanNotification();
|
Configure_ScanNotification();
|
||||||
@ -50,6 +49,8 @@ namespace LibationWinForms
|
|||||||
Configure_Settings();
|
Configure_Settings();
|
||||||
Configure_ProcessQueue();
|
Configure_ProcessQueue();
|
||||||
Configure_Filter();
|
Configure_Filter();
|
||||||
|
// misc which belongs in winforms app but doesn't have a UI element
|
||||||
|
Configure_NonUI();
|
||||||
|
|
||||||
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
// Configure_Grid(); // since it's just this, can keep here. If it needs more, then give grid it's own 'partial class Form1'
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user