Skip to content

Commit 628cc31

Browse files
committed
feat: update attachment functionality to support website tabs
- Bumped application version from 0.1.1 to 0.2.0. - Added support for attaching notes to visible website tabs in addition to processes and folders. - Enhanced UI to include options for selecting and attaching to website tabs. - Updated localization for new attachment options related to website tabs. - Refactored attachment logic to accommodate window titles and improve user experience.
1 parent e0234b9 commit 628cc31

7 files changed

Lines changed: 398 additions & 19 deletions

‎Lang.cs‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,11 +326,14 @@ public static class Lang
326326
// ── Attachment ──────────────────────────────────────────
327327
["attach_to_window"] = "Attach to window",
328328
["attach_to_program"] = "Attach to program",
329+
["attach_to_website"] = "Attach to website tab",
329330
["attach_to_folder"] = "Attach to folder",
330331
["attached_to"] = "Attached to",
331332
["detach"] = "Detach",
332333
["select_program"] = "Select a program",
333334
["no_windows_found"] = "No windows found",
335+
["select_website"] = "Select a website tab",
336+
["no_web_tabs_found"] = "No web tabs found",
334337
},
335338

336339
["fr"] = new()
@@ -651,13 +654,16 @@ public static class Lang
651654
["ai_prompt_edit"] = "Modifier",
652655

653656
// ── Attachment ──────────────────────────────────────────
654-
["attach_to_window"] = "Attacher à une fenêtre",
657+
["attach_to_window"] = "Attacher",
655658
["attach_to_program"] = "Attacher à un programme",
659+
["attach_to_website"] = "Attacher à un onglet web",
656660
["attach_to_folder"] = "Attacher à un dossier",
657661
["attached_to"] = "Attaché à",
658662
["detach"] = "Détacher",
659663
["select_program"] = "Sélectionner un programme",
660664
["no_windows_found"] = "Aucune fenêtre trouvée",
665+
["select_website"] = "Sélectionner un onglet web",
666+
["no_web_tabs_found"] = "Aucun onglet web trouvé",
661667
}
662668
};
663669

‎MainWindow.xaml.cs‎

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ private UIElement CreateNoteCard(NoteEntry note)
456456
VerticalAlignment = VerticalAlignment.Center
457457
};
458458
ToolTipService.SetToolTip(attachIcon,
459-
$"{Lang.T("attached_to")} {(note.AttachMode == "process" ? note.AttachTarget : Path.GetFileName(note.AttachTarget))}");
459+
$"{Lang.T("attached_to")} {GetAttachTargetLabel(note)}");
460460
metaPanel.Children.Add(attachIcon);
461461
}
462462
if (!string.IsNullOrEmpty(note.TaskProgress))
@@ -552,6 +552,20 @@ private static async Task LoadThumbnailAsync(Image img, byte[] data)
552552
catch { }
553553
}
554554

555+
private static string GetAttachTargetLabel(NoteEntry note)
556+
{
557+
if (string.IsNullOrWhiteSpace(note.AttachTarget))
558+
return "";
559+
560+
return note.AttachMode switch
561+
{
562+
"process" => note.AttachTarget!,
563+
"folder" => Path.GetFileName(note.AttachTarget!),
564+
"title" => note.AttachTarget!,
565+
_ => note.AttachTarget!
566+
};
567+
}
568+
555569
private void ShowNoteContextMenu(string noteId, FrameworkElement target)
556570
{
557571
var note = _notes.Notes.FirstOrDefault(n => n.Id == noteId);
@@ -585,8 +599,8 @@ private void ShowNoteContextMenu(string noteId, FrameworkElement target)
585599
HandleLockToggle(noteId, target);
586600
}),
587601
new("\uE723",
588-
!string.IsNullOrEmpty(note?.AttachMode)
589-
? $"{Lang.T("attached_to")} {(note.AttachMode == "process" ? note.AttachTarget : Path.GetFileName(note.AttachTarget))}"
602+
note != null && !string.IsNullOrEmpty(note.AttachMode)
603+
? $"{Lang.T("attached_to")} {GetAttachTargetLabel(note)}"
590604
: Lang.T("attach_to_window"), [], () =>
591605
{
592606
if (note != null)
@@ -635,6 +649,10 @@ private void ShowAttachMenuFromList(NoteEntry note, FrameworkElement target)
635649
{
636650
ShowRunningProgramsPicker(note, target);
637651
}),
652+
new("\uE774", Lang.T("attach_to_website"), [], () =>
653+
{
654+
ShowWebTabsPicker(note, target);
655+
}),
638656
new("\uE8B7", Lang.T("attach_to_folder"), [], async () =>
639657
{
640658
await PickAttachFolder(note);
@@ -687,6 +705,34 @@ private void ShowRunningProgramsPicker(NoteEntry note, FrameworkElement target)
687705
flyout.ShowAt(target);
688706
}
689707

708+
private void ShowWebTabsPicker(NoteEntry note, FrameworkElement target)
709+
{
710+
var tabs = WindowAttachmentHelper.GetVisibleWebTabs();
711+
if (tabs.Count == 0)
712+
{
713+
var empty = ActionPanel.Create(Lang.T("select_website"),
714+
[new(null, Lang.T("no_web_tabs_found"), [], () => { })]);
715+
empty.ShowAt(target);
716+
return;
717+
}
718+
719+
var actions = tabs.Select(tab =>
720+
new ActionPanel.ActionItem(null, $"{tab.ProcessName} — {tab.Title}", [], () =>
721+
{
722+
note.AttachTarget = tab.Title;
723+
note.AttachMode = "title";
724+
note.AttachOffsetX = 0;
725+
note.AttachOffsetY = 0;
726+
_notes.Save();
727+
_attachmentService?.Refresh();
728+
RefreshCurrentView();
729+
})
730+
).ToList();
731+
732+
var flyout = ActionPanel.Create(Lang.T("select_website"), actions);
733+
flyout.ShowAt(target);
734+
}
735+
690736
private async Task PickAttachFolder(NoteEntry note)
691737
{
692738
var picker = new Windows.Storage.Pickers.FolderPicker();
@@ -884,6 +930,12 @@ private void HandleNoteAttachmentChanged(NoteWindow window)
884930
window.SetPinnedOnTop(true);
885931
_attachmentService?.Refresh();
886932
}
933+
else if (note.AttachMode == "title")
934+
{
935+
// Check if the foreground window title matches
936+
window.SetPinnedOnTop(true);
937+
_attachmentService?.Refresh();
938+
}
887939
}
888940

889941
// ── Window attachment ───────────────────────────────────────

‎NoteWindow.xaml.cs‎

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1782,7 +1782,7 @@ private void Menu_Click(object sender, RoutedEventArgs e)
17821782
}),
17831783
new("\uE723",
17841784
!string.IsNullOrEmpty(_note.AttachMode)
1785-
? $"{Lang.T("attached_to")} {(_note.AttachMode == "process" ? _note.AttachTarget : System.IO.Path.GetFileName(_note.AttachTarget))}"
1785+
? $"{Lang.T("attached_to")} {GetAttachTargetLabel()}"
17861786
: Lang.T("attach_to_window"), [], () => ShowAttachMenu()),
17871787
new("\uE7B8", _note.IsArchived ? Lang.T("unarchive") : Lang.T("archive"), [], () =>
17881788
{
@@ -1810,6 +1810,7 @@ private void ShowAttachMenu()
18101810
var actions = new List<ActionPanel.ActionItem>
18111811
{
18121812
new("\uE737", Lang.T("attach_to_program"), [], () => ShowRunningProgramsList()),
1813+
new("\uE774", Lang.T("attach_to_website"), [], () => ShowWebTabsList()),
18131814
new("\uE8B7", Lang.T("attach_to_folder"), [], async () => await PickAttachFolder()),
18141815
};
18151816

@@ -1861,6 +1862,35 @@ private void ShowRunningProgramsList()
18611862
flyout.ShowAt(MenuButton);
18621863
}
18631864

1865+
private void ShowWebTabsList()
1866+
{
1867+
var tabs = WindowAttachmentHelper.GetVisibleWebTabs();
1868+
if (tabs.Count == 0)
1869+
{
1870+
var empty = ActionPanel.Create(Lang.T("select_website"),
1871+
[new(null, Lang.T("no_web_tabs_found"), [], () => { })]);
1872+
empty.ShowAt(MenuButton);
1873+
return;
1874+
}
1875+
1876+
var actions = tabs.Select(tab =>
1877+
new ActionPanel.ActionItem(null, $"{tab.ProcessName} — {tab.Title}", [], () =>
1878+
{
1879+
_note.AttachTarget = tab.Title;
1880+
_note.AttachMode = "title";
1881+
_note.AttachOffsetX = 0;
1882+
_note.AttachOffsetY = 0;
1883+
_notesManager.Save();
1884+
UpdateAttachIcon();
1885+
NoteChanged?.Invoke();
1886+
AttachmentChanged?.Invoke();
1887+
})
1888+
).ToList();
1889+
1890+
var flyout = ActionPanel.Create(Lang.T("select_website"), actions);
1891+
flyout.ShowAt(MenuButton);
1892+
}
1893+
18641894
private async Task PickAttachFolder()
18651895
{
18661896
var picker = new Windows.Storage.Pickers.FolderPicker();
@@ -1900,14 +1930,25 @@ private void UpdateAttachIcon()
19001930

19011931
if (!string.IsNullOrEmpty(_note.AttachMode))
19021932
{
1903-
var target = _note.AttachMode == "process"
1904-
? _note.AttachTarget
1905-
: System.IO.Path.GetFileName(_note.AttachTarget);
1906-
ToolTipService.SetToolTip(AttachIcon, $"{Lang.T("attached_to")} {target}");
1933+
ToolTipService.SetToolTip(AttachIcon, $"{Lang.T("attached_to")} {GetAttachTargetLabel()}");
19071934
}
19081935
}
19091936
}
19101937

1938+
private string GetAttachTargetLabel()
1939+
{
1940+
if (string.IsNullOrWhiteSpace(_note.AttachTarget))
1941+
return "";
1942+
1943+
return _note.AttachMode switch
1944+
{
1945+
"process" => _note.AttachTarget!,
1946+
"folder" => System.IO.Path.GetFileName(_note.AttachTarget!),
1947+
"title" => _note.AttachTarget!,
1948+
_ => _note.AttachTarget!
1949+
};
1950+
}
1951+
19111952
private void ShowTaskReminderDialog(TaskItem task, FontIcon bellIcon, Button reminderBtn)
19121953
{
19131954
var tertiaryBrush = (Brush)Application.Current.Resources["TextFillColorTertiaryBrush"];

‎NotesManager.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,8 @@ public class NoteEntry
494494
public bool IsCompact { get; set; }
495495

496496
// Attachment: attach note to a running process or an Explorer folder
497-
public string? AttachTarget { get; set; } // process name (e.g. "notepad") or folder path
498-
public string? AttachMode { get; set; } // "process" | "folder" | null
497+
public string? AttachTarget { get; set; } // process name, window title fragment, or folder path
498+
public string? AttachMode { get; set; } // "process" | "title" | "folder" | null
499499
public int AttachOffsetX { get; set; } // relative X offset to target window
500500
public int AttachOffsetY { get; set; } // relative Y offset to target window
501501

‎WindowAttachmentHelper.cs‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@ namespace NoteUI;
1010
public static class WindowAttachmentHelper
1111
{
1212
public record WindowInfo(string ProcessName, string Title);
13+
public record WebTabInfo(string ProcessName, string Title);
14+
15+
private static readonly HashSet<string> BrowserProcesses = new(StringComparer.OrdinalIgnoreCase)
16+
{
17+
"chrome",
18+
"msedge",
19+
"firefox",
20+
"brave",
21+
"opera",
22+
"arc",
23+
"vivaldi",
24+
"chromium",
25+
"iexplore",
26+
};
1327

1428
public static List<WindowInfo> GetVisibleWindows()
1529
{
@@ -51,6 +65,74 @@ public static List<WindowInfo> GetVisibleWindows()
5165
return results.OrderBy(w => w.ProcessName, StringComparer.OrdinalIgnoreCase).ToList();
5266
}
5367

68+
public static List<WebTabInfo> GetVisibleWebTabs()
69+
{
70+
var results = new List<WebTabInfo>();
71+
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
72+
var currentPid = Environment.ProcessId;
73+
74+
EnumWindows((hwnd, _) =>
75+
{
76+
if (!IsWindowVisible(hwnd)) return true;
77+
78+
var sb = new StringBuilder(512);
79+
GetWindowText(hwnd, sb, sb.Capacity);
80+
var title = NormalizeBrowserTitle(sb.ToString());
81+
if (string.IsNullOrWhiteSpace(title)) return true;
82+
83+
GetWindowThreadProcessId(hwnd, out var pid);
84+
if (pid == 0 || pid == currentPid) return true;
85+
86+
try
87+
{
88+
var proc = Process.GetProcessById((int)pid);
89+
var name = proc.ProcessName;
90+
if (!BrowserProcesses.Contains(name))
91+
return true;
92+
93+
var dedupKey = $"{name}|{title}";
94+
if (seen.Add(dedupKey))
95+
results.Add(new WebTabInfo(name, title));
96+
}
97+
catch { }
98+
99+
return true;
100+
}, IntPtr.Zero);
101+
102+
return results
103+
.OrderBy(w => w.ProcessName, StringComparer.OrdinalIgnoreCase)
104+
.ThenBy(w => w.Title, StringComparer.OrdinalIgnoreCase)
105+
.ToList();
106+
}
107+
108+
private static string NormalizeBrowserTitle(string title)
109+
{
110+
var normalized = title.Trim();
111+
if (normalized.Length == 0)
112+
return normalized;
113+
114+
string[] suffixes =
115+
[
116+
" - Google Chrome",
117+
" - Microsoft Edge",
118+
" - Mozilla Firefox",
119+
" - Brave",
120+
" - Opera",
121+
" - Arc",
122+
" - Vivaldi",
123+
" - Chromium",
124+
" - Internet Explorer"
125+
];
126+
127+
foreach (var suffix in suffixes)
128+
{
129+
if (normalized.EndsWith(suffix, StringComparison.OrdinalIgnoreCase))
130+
return normalized[..^suffix.Length].Trim();
131+
}
132+
133+
return normalized;
134+
}
135+
54136
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
55137

56138
[DllImport("user32.dll")]

0 commit comments

Comments
 (0)