Windowsウィジェット/タスク管理

ウィジェット

TaskWidget.ps1

<#
============================================================
  TaskWidget.ps1 - Desktop Task Widget (Windows Native)
============================================================
#>

Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore
Add-Type -AssemblyName WindowsBase
Add-Type -AssemblyName System.Windows.Forms

# --- Data ---
$script:DataDir = Join-Path $env:USERPROFILE ".taskwidget"
$script:DataFile = Join-Path $script:DataDir "tasks.json"
if (-not (Test-Path $script:DataDir)) {
    New-Item -ItemType Directory -Path $script:DataDir -Force | Out-Null
}

function Load-Tasks {
    if (Test-Path $script:DataFile) {
        try {
            $json = Get-Content $script:DataFile -Raw -Encoding UTF8
            $tasks = $json | ConvertFrom-Json
            if ($tasks -isnot [Array]) { $tasks = @($tasks) }
            return $tasks
        } catch { return @() }
    }
    return @(
        [PSCustomObject]@{ Id=[guid]::NewGuid().ToString(); Text=([char]0x30BF)+([char]0x30B9)+([char]0x30AF)+([char]0x3092)+([char]0x8FFD)+([char]0x52A0)+([char]0x3057)+([char]0x3066)+([char]0x307F)+([char]0x3088)+([char]0x3046); Done=$false; DueDate="" },
        [PSCustomObject]@{ Id=[guid]::NewGuid().ToString(); Text=([char]0x671F)+([char]0x9650)+([char]0x4ED8)+([char]0x304D)+([char]0x30BF)+([char]0x30B9)+([char]0x30AF)+([char]0x306E)+([char]0x30C6)+([char]0x30B9)+([char]0x30C8); Done=$false; DueDate=(Get-Date).AddDays(1).ToString("yyyy-MM-dd") }
    )
}

$script:Tasks = [System.Collections.ArrayList]@(Load-Tasks)
$script:DataFilePath = $script:DataFile

# --- Shared state via hashtable (closure-safe) ---
$app = @{
    Tasks    = $script:Tasks
    DataFile = $script:DataFilePath
}

function Save-TasksTo {
    param($Tasks, $Path)
    $Tasks | ConvertTo-Json -Depth 3 | Out-File $Path -Encoding UTF8 -Force
}

# --- Toast ---
function Show-ToastNotification {
    param([string]$Title, [string]$Message)
    try {
        [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
        [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType = WindowsRuntime] | Out-Null
        $template = @"
<toast duration="long">
  <visual>
    <binding template="ToastGeneric">
      <text>$Title</text>
      <text>$Message</text>
    </binding>
  </visual>
</toast>
"@
        $xml = New-Object Windows.Data.Xml.Dom.XmlDocument
        $xml.LoadXml($template)
        $toast = New-Object Windows.UI.Notifications.ToastNotification $xml
        [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("TaskWidget").Show($toast)
    } catch {
        try {
            $balloon = New-Object System.Windows.Forms.NotifyIcon
            $balloon.Icon = [System.Drawing.SystemIcons]::Information
            $balloon.BalloonTipTitle = $Title
            $balloon.BalloonTipText = $Message
            $balloon.Visible = $true
            $balloon.ShowBalloonTip(5000)
            Start-Sleep -Seconds 6
            $balloon.Dispose()
        } catch {}
    }
}

function Check-DueReminders {
    $today = (Get-Date).Date
    foreach ($task in $app.Tasks) {
        if (-not $task.Done -and $task.DueDate) {
            try {
                $due = [DateTime]::Parse($task.DueDate)
                $diff = ($due.Date - $today).Days
                $lToday = ([char]0x4ECA)+([char]0x65E5)+([char]0x304C)+([char]0x671F)+([char]0x9650)+([char]0xFF01)
                $lTomorrow = ([char]0x660E)+([char]0x65E5)+([char]0x304C)+([char]0x671F)+([char]0x9650)
                $lOverdue = ([char]0x671F)+([char]0x9650)+([char]0x8D85)+([char]0x904E)+([char]0xFF01)
                $lDays = ([char]0x65E5)+([char]0x8D85)+([char]0x904E)
                if ($diff -eq 0) { Show-ToastNotification $lToday $task.Text }
                elseif ($diff -eq 1) { Show-ToastNotification $lTomorrow $task.Text }
                elseif ($diff -lt 0) { Show-ToastNotification $lOverdue "$($task.Text) ($(([Math]::Abs($diff)))$lDays)" }
            } catch {}
        }
    }
}

# --- XAML ---
[xml]$xaml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="TaskWidget" Width="340" Height="520"
    WindowStyle="None" AllowsTransparency="True" Background="Transparent"
    Topmost="True" ShowInTaskbar="False" ResizeMode="NoResize">
  <Border CornerRadius="12" Background="#E8141422" Margin="8">
    <Border.Effect>
      <DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.5"/>
    </Border.Effect>
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>

      <Border Name="DragArea" Grid.Row="0" CornerRadius="12,12,0,0"
        Background="#2D2D48" Padding="16,12">
        <StackPanel IsHitTestVisible="False">
          <StackPanel Orientation="Horizontal">
            <TextBlock Name="ClockText" Text="00:00" FontSize="36"
              FontWeight="Light" Foreground="#E6E6F0" FontFamily="Segoe UI"/>
            <TextBlock Name="SecondsText" Text=":00" FontSize="18"
              FontWeight="Light" Foreground="#A0A0B4"
              VerticalAlignment="Bottom" Margin="3,0,0,6"/>
          </StackPanel>
          <TextBlock Name="DateText" Text="" FontSize="12"
            Foreground="#A0A0B4" FontFamily="Segoe UI"/>
        </StackPanel>
      </Border>

      <Border Grid.Row="1" Height="1" Background="#3C3C50" Margin="16,0"/>

      <StackPanel Grid.Row="2" Margin="12,10">
        <DockPanel>
          <Button Name="AddButton" DockPanel.Dock="Right" Content="+"
            Width="34" Height="34" Margin="6,0,0,0"
            Background="#64B4FF" Foreground="White" FontSize="16" FontWeight="Bold"/>
          <TextBox Name="TaskInput" Background="#2A2A3E" Foreground="#E6E6F0"
            BorderBrush="#4A4A6A" BorderThickness="1" Padding="8,6"
            FontSize="12" FontFamily="Segoe UI" VerticalContentAlignment="Center"/>
        </DockPanel>
        <StackPanel Orientation="Horizontal" Margin="0,6,0,0">
          <TextBlock Name="DueDateLabel" Foreground="#A0A0B4" FontSize="11"
            VerticalAlignment="Center" Margin="0,0,4,0"/>
          <TextBox Name="DueDateInput" Width="100" Background="#2A2A3E"
            Foreground="#E6E6F0" BorderBrush="#4A4A6A" BorderThickness="1"
            Padding="4,3" FontSize="11" FontFamily="Segoe UI"/>
          <TextBlock Text="  (yyyy-MM-dd)" Foreground="#606078" FontSize="10"
            VerticalAlignment="Center"/>
        </StackPanel>
      </StackPanel>

      <ScrollViewer Grid.Row="3" Margin="4,0" VerticalScrollBarVisibility="Auto">
        <StackPanel Name="TaskList" Margin="8,0"/>
      </ScrollViewer>

      <DockPanel Grid.Row="4" Margin="12,8">
        <Button Name="CloseBtn" DockPanel.Dock="Right" Content="&#x2715;"
          Width="28" Height="28" Background="#33FFFFFF" Foreground="#A0A0B4"
          FontSize="12" BorderThickness="0"/>
        <Button Name="MinimizeBtn" DockPanel.Dock="Right" Content="&#x2500;"
          Width="28" Height="28" Margin="0,0,4,0" Background="#33FFFFFF"
          Foreground="#A0A0B4" FontSize="14" BorderThickness="0"/>
        <TextBlock Name="TaskCount" Text="0" Foreground="#A0A0B4"
          FontSize="11" VerticalAlignment="Center"/>
      </DockPanel>
    </Grid>
  </Border>
</Window>
"@

$reader = New-Object System.Xml.XmlNodeReader $xaml
$window = [System.Windows.Markup.XamlReader]::Load($reader)

$clockText    = $window.FindName("ClockText")
$secondsText  = $window.FindName("SecondsText")
$dateText     = $window.FindName("DateText")
$taskInput    = $window.FindName("TaskInput")
$dueDateInput = $window.FindName("DueDateInput")
$dueDateLabel = $window.FindName("DueDateLabel")
$addButton    = $window.FindName("AddButton")
$taskList     = $window.FindName("TaskList")
$taskCount    = $window.FindName("TaskCount")
$minimizeBtn  = $window.FindName("MinimizeBtn")
$closeBtn     = $window.FindName("CloseBtn")
$dragArea     = $window.FindName("DragArea")

$dueDateLabel.Text = ([char]0x671F)+([char]0x9650)+":"

$dragArea.Add_MouseLeftButtonDown({ $window.DragMove() })
$closeBtn.Add_Click({ $window.Close() })
$minimizeBtn.Add_Click({ $window.WindowState = "Minimized" })

# --- Local refs for closures (NO $script: inside GetNewClosure) ---
$tasksRef     = $app.Tasks
$dataFileRef  = $app.DataFile
$taskListRef  = $taskList
$taskCountRef = $taskCount
$taskInputRef = $taskInput
$dueDateRef   = $dueDateInput

$script:RenderTaskList = {
    $taskListRef.Children.Clear()
    $remaining = 0
    $renderRef = $script:RenderTaskList

    for ($i = 0; $i -lt $tasksRef.Count; $i++) {
        $task = $tasksRef[$i]
        $idx = $i
        $taskId = $task.Id
        if (-not $task.Done) { $remaining++ }

        $rowBorder = New-Object System.Windows.Controls.Border
        $rowBorder.CornerRadius = [System.Windows.CornerRadius]::new(6)
        $rowBorder.Padding = [System.Windows.Thickness]::new(4, 6, 4, 6)
        $rowBorder.Margin = [System.Windows.Thickness]::new(0, 2, 0, 2)
        if ($task.Done) {
            $rowBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#1A1A2A")
        } else {
            $rowBorder.Background = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#222238")
        }

        $row = New-Object System.Windows.Controls.DockPanel

        # --- Move up/down ---
        $movePanel = New-Object System.Windows.Controls.StackPanel
        $movePanel.VerticalAlignment = "Center"
        $movePanel.Margin = [System.Windows.Thickness]::new(0, 0, 2, 0)
        [System.Windows.Controls.DockPanel]::SetDock($movePanel, "Left")

        $upBtn = New-Object System.Windows.Controls.Button
        $upBtn.Content = ([char]0x25B2)
        $upBtn.FontSize = 8
        $upBtn.Background = [System.Windows.Media.Brushes]::Transparent
        $upBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
        $upBtn.BorderThickness = [System.Windows.Thickness]::new(0)
        $upBtn.Cursor = [System.Windows.Input.Cursors]::Hand
        $upBtn.Padding = [System.Windows.Thickness]::new(3, 0, 3, 0)
        $upBtn.Tag = $idx
        $upBtn.Add_Click({
            param($s, $e)
            $ci = $s.Tag
            if ($ci -gt 0) {
                $item = $tasksRef[$ci]
                $tasksRef.RemoveAt($ci)
                $tasksRef.Insert($ci - 1, $item)
                Save-TasksTo $tasksRef $dataFileRef
                & $renderRef
            }
        }.GetNewClosure())

        $downBtn = New-Object System.Windows.Controls.Button
        $downBtn.Content = ([char]0x25BC)
        $downBtn.FontSize = 8
        $downBtn.Background = [System.Windows.Media.Brushes]::Transparent
        $downBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
        $downBtn.BorderThickness = [System.Windows.Thickness]::new(0)
        $downBtn.Cursor = [System.Windows.Input.Cursors]::Hand
        $downBtn.Padding = [System.Windows.Thickness]::new(3, 0, 3, 0)
        $downBtn.Tag = $idx
        $downBtn.Add_Click({
            param($s, $e)
            $ci = $s.Tag
            if ($ci -lt ($tasksRef.Count - 1)) {
                $item = $tasksRef[$ci]
                $tasksRef.RemoveAt($ci)
                $tasksRef.Insert($ci + 1, $item)
                Save-TasksTo $tasksRef $dataFileRef
                & $renderRef
            }
        }.GetNewClosure())

        $movePanel.Children.Add($upBtn) | Out-Null
        $movePanel.Children.Add($downBtn) | Out-Null

        # --- Delete ---
        $delBtn = New-Object System.Windows.Controls.Button
        $delBtn.Content = "X"
        $delBtn.FontSize = 11
        $delBtn.Background = [System.Windows.Media.Brushes]::Transparent
        $delBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
        $delBtn.BorderThickness = [System.Windows.Thickness]::new(0)
        $delBtn.Cursor = [System.Windows.Input.Cursors]::Hand
        $delBtn.Padding = [System.Windows.Thickness]::new(2)
        $delBtn.VerticalAlignment = "Center"
        $delBtn.Opacity = 0.5
        $delBtn.Tag = $taskId
        [System.Windows.Controls.DockPanel]::SetDock($delBtn, "Right")
        $delBtn.Add_Click({
            param($s, $e)
            $id = $s.Tag
            $toRemove = $null
            foreach ($t in $tasksRef) {
                if ($t.Id -eq $id) { $toRemove = $t; break }
            }
            if ($toRemove) {
                $tasksRef.Remove($toRemove) | Out-Null
                Save-TasksTo $tasksRef $dataFileRef
                & $renderRef
            }
        }.GetNewClosure())
        $delBtn.Add_MouseEnter({ $this.Opacity = 1.0 })
        $delBtn.Add_MouseLeave({ $this.Opacity = 0.5 })

        # --- Check ---
        if ($task.Done) { $mark = [char]0x2705 } else { $mark = [char]0x2B1C }
        $checkBtn = New-Object System.Windows.Controls.Button
        $checkBtn.Content = $mark
        $checkBtn.FontSize = 14
        $checkBtn.Background = [System.Windows.Media.Brushes]::Transparent
        $checkBtn.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
        $checkBtn.BorderThickness = [System.Windows.Thickness]::new(0)
        $checkBtn.Cursor = [System.Windows.Input.Cursors]::Hand
        $checkBtn.Padding = [System.Windows.Thickness]::new(2)
        $checkBtn.VerticalAlignment = "Center"
        $checkBtn.Width = 28
        $checkBtn.Tag = $taskId
        [System.Windows.Controls.DockPanel]::SetDock($checkBtn, "Left")
        $checkBtn.Add_Click({
            param($s, $e)
            $id = $s.Tag
            foreach ($t in $tasksRef) {
                if ($t.Id -eq $id) { $t.Done = -not $t.Done; break }
            }
            Save-TasksTo $tasksRef $dataFileRef
            & $renderRef
        }.GetNewClosure())

        # --- Text ---
        $textPanel = New-Object System.Windows.Controls.StackPanel
        $textPanel.VerticalAlignment = "Center"
        $textPanel.Margin = [System.Windows.Thickness]::new(4, 0, 4, 0)

        $textBlock = New-Object System.Windows.Controls.TextBlock
        $textBlock.Text = $task.Text
        $textBlock.FontSize = 12
        $textBlock.FontFamily = [System.Windows.Media.FontFamily]::new("Segoe UI")
        $textBlock.TextWrapping = "Wrap"
        if ($task.Done) {
            $textBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#606078")
            $textBlock.TextDecorations = [System.Windows.TextDecorations]::Strikethrough
        } else {
            $textBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#E6E6F0")
        }
        $textPanel.Children.Add($textBlock) | Out-Null

        if ($task.DueDate -and -not $task.Done) {
            $dueBlock = New-Object System.Windows.Controls.TextBlock
            $dueBlock.FontSize = 10
            $dueBlock.FontFamily = [System.Windows.Media.FontFamily]::new("Segoe UI")
            try {
                $due = [DateTime]::Parse($task.DueDate)
                $diff = ($due.Date - (Get-Date).Date).Days
                $sOv = ([char]0x671F)+([char]0x9650)+([char]0x8D85)+([char]0x904E)+": "
                $sDs = ([char]0x65E5)+([char]0x8D85)+([char]0x904E)
                $sTd = ([char]0x4ECA)+([char]0x65E5)+([char]0x307E)+([char]0x3067)
                $sTm = ([char]0x660E)+([char]0x65E5)+([char]0x307E)+([char]0x3067)
                $sDl = ([char]0x65E5)+([char]0x5F8C)
                if ($diff -lt 0) {
                    $dueBlock.Text = "$sOv$(([Math]::Abs($diff)))$sDs"
                    $dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FF6464")
                } elseif ($diff -eq 0) {
                    $dueBlock.Text = $sTd
                    $dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FFB040")
                } elseif ($diff -eq 1) {
                    $dueBlock.Text = $sTm
                    $dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#FFB040")
                } else {
                    $dueBlock.Text = "${diff}$sDl ($($due.ToString('M/d')))"
                    $dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
                }
            } catch {
                $kl = ([char]0x671F)+([char]0x9650)+": "
                $dueBlock.Text = "$kl$($task.DueDate)"
                $dueBlock.Foreground = [System.Windows.Media.BrushConverter]::new().ConvertFromString("#A0A0B4")
            }
            $textPanel.Children.Add($dueBlock) | Out-Null
        }

        $row.Children.Add($movePanel) | Out-Null
        $row.Children.Add($delBtn) | Out-Null
        $row.Children.Add($checkBtn) | Out-Null
        $row.Children.Add($textPanel) | Out-Null
        $rowBorder.Child = $row
        $taskListRef.Children.Add($rowBorder) | Out-Null
    }

    $lbl = ([char]0x306E)+([char]0x672A)+([char]0x5B8C)+([char]0x4E86)+([char]0x30BF)+([char]0x30B9)+([char]0x30AF)
    $taskCountRef.Text = "$remaining $lbl"
}

# --- Add task (also uses local refs only) ---
$addButton.Add_Click({
    $text = $taskInputRef.Text.Trim()
    if ($text -eq "") { return }
    $newTask = [PSCustomObject]@{
        Id      = [guid]::NewGuid().ToString()
        Text    = $text
        Done    = $false
        DueDate = $dueDateRef.Text.Trim()
    }
    $tasksRef.Add($newTask) | Out-Null
    Save-TasksTo $tasksRef $dataFileRef
    $taskInputRef.Text = ""
    $dueDateRef.Text = ""
    & $script:RenderTaskList
})

$taskInput.Add_KeyDown({
    param($s, $e)
    if ($e.Key -eq "Return") {
        $text = $taskInputRef.Text.Trim()
        if ($text -eq "") { return }
        $newTask = [PSCustomObject]@{
            Id      = [guid]::NewGuid().ToString()
            Text    = $text
            Done    = $false
            DueDate = $dueDateRef.Text.Trim()
        }
        $tasksRef.Add($newTask) | Out-Null
        Save-TasksTo $tasksRef $dataFileRef
        $taskInputRef.Text = ""
        $dueDateRef.Text = ""
        & $script:RenderTaskList
    }
})

# --- Clock ---
$timer = New-Object System.Windows.Threading.DispatcherTimer
$timer.Interval = [TimeSpan]::FromSeconds(1)
$timer.Add_Tick({
    $now = Get-Date
    $clockText.Text = $now.ToString("HH:mm")
    $secondsText.Text = ":" + $now.ToString("ss")
    $y = ([char]0x5E74); $mo = ([char]0x6708); $dy = ([char]0x65E5)
    $dow = switch ($now.DayOfWeek) {
        "Sunday"    { ([char]0x65E5) }
        "Monday"    { ([char]0x6708) }
        "Tuesday"   { ([char]0x706B) }
        "Wednesday" { ([char]0x6C34) }
        "Thursday"  { ([char]0x6728) }
        "Friday"    { ([char]0x91D1) }
        "Saturday"  { ([char]0x571F) }
    }
    $dateText.Text = "$($now.Year)$y$($now.Month)$mo$($now.Day)$dy ($dow)"
})
$timer.Start()

# --- Init ---
$window.Add_Loaded({
    $screen = [System.Windows.SystemParameters]::WorkArea
    $window.Left = $screen.Right - $window.Width - 16
    $window.Top = $screen.Bottom - $window.Height - 16
    & $script:RenderTaskList
    Check-DueReminders
})

$window.ShowDialog() | Out-Null

Launch-TaskWidget.bat

@echo off
powershell -ExecutionPolicy Bypass -WindowStyle Hidden -File "%~dp0TaskWidget.ps1"