ebooks.psm1

param([String]$SCRIPT:addFolder = 'C:\books')
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
   Write-Host "Module ebooks.psm1 removed on $(Get-Date)"
}

function Search-Book {
<#
.SYNOPSIS
If you have a large collection of e-books, then locating the one that you want can prove time-consuming if you do
not have an appropriate tool to help you. Search-Book is a basic application that lets you search for any word in
a book title. The software searches for the entered word in all titles in the directory, including its sub-folders.
.NOTES
A very much modified WPF script from 'PowerShell In Action Second Edition' by Bruce Payette. The original example
(Select-String) is described on pages 754-757.
Search-Book will locate any filtered file names containing text entered into the Author textbox and display them
in two columns. The WPF file associated with this script is 'searchbook.xaml'.
Important: to ensure that the Taskbar notifications are displayed for Powershell: right-click the Taskbar then
Properties, Customize; scroll down to Windows Powershell and select Show Icon and Notifications, then OK to quit.
This package should contain the following files: NewOranges.jpg, searchbook.xaml, ebooks.psm1, ebooks.psd1. Note that
the Gridview table may occasionally hide behind the Powershell logo in the taskbar. Just click it to load.
.DESCRIPTION
The application automatically adds any top-level folders named Books on all drives, including USB ones, to a drop-down
list named 'Search Folder' on its Control Panel. Please note that it does not add sub-folders even if they are named
Books. In such a case, it will only display the drive letter, and you will need to use the Folders button to select
sub-folders.
Using the Folders button produces the standard 'Browse For Folder' dialog box from which a folder is selected. Note
that there are certain restrictions here, ie it is not possible to select just the C:\ drive, for example (it would
take too long to search); and any folder in the Windows directory is not allowed.
To add a new default folder simply use 'Import-Module -Name ebooks -Force -ArgumentList D:\MyFolder' for example.
The whole directory tree from that entered in the Path box will be searched for either *.epub, *.mobi, *.azw3, *.azw,
*.apnx, *.pdf or *.txt files. Any of these file names containing the text string from the Author box will be
displayed together with the total number of items. Clicking 'OK' on the display will now close it and instead
open the folder (in Windows Explorer) containing the selected book.
To close the entire filtered list click either the red 'X' at the top or the Cancel button. To close the Control Panel
just click the Exit button,the Navigation Bar icon or use the Esc key.
It will be necessary to wait about 5 seconds before starting Search-Book a second time since the current session will
take this time to end cleanly.
Check 'Get-Help Search-Book -Full' for further information about certain installation details.
.LINK
http://www.SeaStar.co.nf
#>

   
   $errorActionPreference = 'Stop'
   Add-Type -Assembly PresentationCore,PresentationFramework,System.Drawing,System.Windows.Forms
   $mode = [System.Threading.Thread]::CurrentThread.ApartmentState
   if ($mode -ne "STA") {
      $script:emsg = "This script can only be run when PowerShell" +
        "`nis started with the -sta switch."
      Write-Warning "Powershell must be started with the -STA switch"
   }

   function Get-Folder() {
      $folderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog
      $folderBrowser.ShowNewFolderButton = $false
      $folderBrowser.description = "Select new Search Folder:"
      [void]$folderBrowser.ShowDialog()
      return $folderBrowser.SelectedPath                #Return '' for cancel button.
      $folderBrowser.Dispose()
   }

   $counter = 0
   $xamlPath = Join-Path $PSScriptroot searchbook.xaml 
   $stream = [System.IO.StreamReader] $xamlPath
   $form = [System.Windows.Markup.XamlReader]::Load($stream.BaseStream)
   $stream.Close()

   $icon = [System.Drawing.Icon]::ExtractAssociatedIcon("$pshome\powershell.exe")
   $script:notifyIcon = New-Object System.Windows.Forms.NotifyIcon 
   $notifyIcon.Icon = $icon 
   $notifyIcon.BalloonTipIcon  = "Info" 
   $notifyIcon.BalloonTipTitle = "Powershell Search-Book"
   $notifyIcon.BalloonTipText  = "Loading Search-Book GUI. Please wait ..."
   $notifyIcon.Visible = $True 
   $notifyIcon.ShowBalloonTip(50)

   $notifyIcon.add_Click({                         
      $notifyIcon.Visible = $false
      $form.Close()
      dispose 0
   })

   $notifyIcon.add_MouseMove({
      $notifyIcon.Text = "Click to Exit"
   })

   $form.AllowsTransparency = $True
   $form.Opacity = 1.0
   $form.WindowStyle = 'None'

   $errors = $form.FindName("Message")
   $errors.Foreground = "blue"
   $errors.Fontweight = "bold"

   $labelSearch = $form.FindName("Label")
   $filepath = $form.FindName("Path")
   $filepath.Tag = 'Folder'
   $browser = $form.FindName("Browse")
   $browser.Tooltip = "Open Folder Dialog"

   Get-CIMinstance -Namespace root\cimv2 -Class Win32_LogicalDisk |     #Get the default path if external USB attached.
      Select-Object VolumeName, DriveType, DeviceID | 
         Where-Object {($_.VolumeName -ne $null) -and ($_.DeviceID -notmatch "^[c]:$") -and ($_.DriveType -match "^[234]$")} |
            foreach {
               $item = New-Object -TypeName "System.Windows.Controls.ComboBoxItem"
               $counter+= 1
               $item.Content = $_.DeviceID + "\"
               $item.ToolTip = "Volume: " + $_.VolumeName
               $checkPath = $_.DeviceID + "\Books"
               if (Test-Path $checkPath -PathType container) {
                  $item.Content = $_.DeviceID + "\Books"
                  $filepath.SelectedIndex = $counter
               } 
               $filePath.AddChild($item)   #Just add drive letter if no books.
            }

   $box = $form.FindName("FileFilter")
   $item = New-Object -TypeName "System.Windows.Controls.ComboBoxItem"
   $item.Content = "*.all"
   $item.ToolTip = "All book types"             #This is just to add a tooltip.
   $box.AddChild($item)

   if ($filepath.Text -eq "") {               #If nothing here then no display.
      $filepath.Text = $home
   } 

   if (Test-Path $addFolder -PathType container) {
      $filepath.AddChild($addFolder)      #Bypass 'C:\' drive exclusion above.
      $filepath.SelectedItem = $addFolder
   } 

   $TextPattern = $form.FindName("TextPattern")
   $TextPattern.Tag = 'Author'
   $TextPattern.Text = ''
   $TextPattern.Select(0, 0)         
   $TextPattern.Focus() | Out-Null
   $TypeFilter = $form.FindName("FileFilter")
   $TypeFilter.Text = '*.epub'

   $run = $form.FindName("Run")
   $image = $form.FindName("Oranges")                    #We need absolute path here.
   $image.Source = Join-Path $PSScriptroot NewOranges.jpg

   $run.add_Click({
      switch ($filepath,$TextPattern) {
        {$_.Text -eq ''} {
            $errors.content = "No search entry for $($_.Tag). Please try again."
            [Console]::Beep(800,500)
            $_.Focus() | Out-Null
            return 
        }
      }
      if (Test-Path $filepath.Text -PathType container) {      #Now all fields valid.
         $form.DialogResult = $true                 #Triggers the ShowDialog() below.
      } else { 
         [Console]::Beep(800,500)                    #Manually typed entry not valid.
         $errors.content = "Folder '$($Filepath.Text)' does not exist. Please try again."
         $TextPattern.Focus() | Out-Null
      }
   })

   $cancel = $form.FindName("Cancel")
   $cancel.add_Click({
       $notifyIcon.Visible = $false 
       $form.Close()
       dispose 0
   })

   $form.Add_MouseLeftButtonDown({
      $form.DragMove()
   })

   $filepath.Add_MouseEnter({
      $errors.Content = ''
   })

   $browser.add_Click({
       $ignore = 'CurrentCultureIgnoreCase'
       $errors.Content = ''
       $saveFolder = $filePath.Text         
       $filepath.Text = Get-Folder
       if ($filepath.Text -eq '' -or $filepath.Items.Contains($filepath.Text)) {   
          $filepath.Text = $saveFolder         #Ignore Folder 'Cancel' and duplicates.
          $filepath.focus() | Out-Null
          return
       }
       $setMatch = $filepath.Text + "*"       #Disallow 'C:\' or 'C:\Windows' folders.
       if ($env:SystemRoot -ilike $setMatch -or                 
            $filepath.Text.StartsWith($env:SystemRoot,$ignore)) {
           $errors.Content = "Invalid folder: '$($filepath.Text)'"
           $filepath.Text = $saveFolder
           [Console]::Beep(800,500)
       } else {
           $filepath.AddChild($filepath.Text)    #Save for use during current session.
       }
       $filepath.focus() | Out-Null
   })

   $Textpattern.Add_MouseEnter({
       $errors.Content = ''
   })

   $form.Left = 0

   function dispose {
      param([INT]$time = 4)
      Start-Sleep -Seconds $time               #Allow time for message to display.
      $script:notifyIcon.Dispose()
   }

   if ($form.ShowDialog()) {
      $filter = $TypeFilter.Text 
      if ($filter -eq '*.all') {
          $property = @{Label = "Book Title"; Expression = {$_.BaseName}},@{Label = "Type"; Expression = {$_.Extension}},'Directory'
          $books  = (Get-ChildItem $Filepath.Text -Recurse ) |
             Where-Object {($_.extension -ieq ".epub" -or 
                            $_.extension -ieq ".azw3" -or
                            $_.extension -ieq ".azw"  -or
                            $_.extension -ieq ".apnx" -or 
                            $_.extension -ieq ".mobi" -or
                            $_.extension -ieq ".prc") -and ($_.FullName -match $Textpattern.Text)}
      } else {
          $property = @{Label = "Book Title"; Expression = {$_.BaseName}},'Directory'
          $books  = (Get-ChildItem $Filepath.Text -Recurse -Filter $filter) | 
             Where-Object {$_.FullName -match $Textpattern.Text}
      }
      $title = "Search-Book [Using filter $($filter)]"
      $text = $Textpattern.Text.Substring(0,1).ToUpper() + $Textpattern.Text.Substring(1) 
      if ($books.Count -gt 0) {
          $notifyIcon.BalloonTipText = "$($books.count) items found for '$($text)' ($filter)"
          $notifyIcon.Visible = $True 
          $notifyIcon.ShowBalloonTip(300) 
          dispose 5                         #No need for icon once GUI closed, but allow time to read message.
          $books | Select-Object -Property $property | Out-Gridview -Title $title -OutputMode single -Outvariable temp |
              ForEach-Object {Invoke-Item $_.Directory}             #User clicked 'OK'
      } else {
          [Console]::Beep(800,500)
          $notifyIcon.BalloonTipText  = "No matches found for author '$($text)'."
          $notifyIcon.Visible = $True 
          $notifyIcon.ShowBalloonTip(300)
          dispose 5
      }
   }
} #End function Search-Book
New-Alias book Search-Book -Description 'Search for any ebooks on the PC'
Export-ModuleMember -Function Search-Book -Alias book