Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ All notable changes to this project will be documented in this file.
- **UI & Layout Updates**:
- Corrected `ft.View` constructor calls to reflect new argument ordering (`controls` is now positional first).
- Replaced `PopupMenuItem(text=...)` with `PopupMenuItem(content=ft.Text(...))`.
- **Migrated `ft.Tabs`**: Overhauled Tabs implementation to use the new `length`, `content`, and `ft.TabBar` structure required by v0.84.0.
- Replaced `Tab(text=...)` with `Tab(label=...)`.
- Migrated legacy `ft.alignment` constants to `ft.Alignment(x, y)` coordinates.
- **Navigation Fixes**:
- Refactored `DiffView` navigation handlers to use `self.page.run_task()` for reliable async execution from button clicks.
Expand Down
36 changes: 32 additions & 4 deletions core/comparator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from pykeepass.entry import Entry
from collections import namedtuple

DiffEntry = namedtuple('DiffEntry', ['uuid', 'title', 'state', 'entry_a', 'entry_b', 'diffs'])
DiffEntry = namedtuple('DiffEntry', ['uuid', 'title', 'state', 'entry_a', 'entry_b', 'diffs', 'ahead'])

class Comparator:
def __init__(self, kp_a: PyKeePass, kp_b: PyKeePass):
Expand All @@ -21,20 +21,48 @@ def compare(self):
entry_b = entries_b.get(uuid)

if entry_a and not entry_b:
diff_results.append(DiffEntry(uuid, entry_a.title, 'ONLY_IN_A', entry_a, None, []))
diff_results.append(DiffEntry(uuid, entry_a.title, 'ONLY_IN_A', entry_a, None, [], 'A'))
elif entry_b and not entry_a:
diff_results.append(DiffEntry(uuid, entry_b.title, 'ONLY_IN_B', None, entry_b, []))
diff_results.append(DiffEntry(uuid, entry_b.title, 'ONLY_IN_B', None, entry_b, [], 'B'))
else:
# In both, compare fields
diffs = self._compare_fields(entry_a, entry_b)
if diffs:
diff_results.append(DiffEntry(uuid, entry_a.title, 'MODIFIED', entry_a, entry_b, diffs))
ahead = self._detect_ahead(entry_a, entry_b)
diff_results.append(DiffEntry(uuid, entry_a.title, 'MODIFIED', entry_a, entry_b, diffs, ahead))
else:
# Identical
pass # We might want to skip identical ones for the diff view

return diff_results

def _detect_ahead(self, a, b):
# 1. Check history
# If a is in b's history, then b is ahead
if self._is_in_history(a, b.history):
return 'B'
# If b is in a's history, then a is ahead
if self._is_in_history(b, a.history):
return 'A'

# 2. Fallback to mtime
if a.mtime > b.mtime:
return 'A'
elif b.mtime > a.mtime:
return 'B'

return None

def _is_in_history(self, entry, history):
if not history:
return False

# Check the last 5 history entries (limit to avoid performance issues)
for h_entry in reversed(history[-5:]):
if not self._compare_fields(entry, h_entry):
return True
return False

def _compare_fields(self, a: Entry, b: Entry):
diffs = []
fields = ['title', 'username', 'password', 'url', 'notes']
Expand Down
85 changes: 75 additions & 10 deletions views/diff_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def __init__(self, page: ft.Page):
self.save_file_picker = ft.FilePicker()
# No longer adding to page.overlay as it is a service in v0.84.0
self.pending_save_target = None
self.current_filter = "all"

self.calculate_diff()
self.setup_ui()
Expand Down Expand Up @@ -69,6 +70,22 @@ def setup_ui(self):
padding=10,
auto_scroll=False
)

# Filter Tabs
self.filter_tabs = ft.Tabs(
selected_index=0,
length=5,
on_change=self.on_filter_change,
content=ft.TabBar(
tabs=[
ft.Tab(label="All"),
ft.Tab(label="Changed"),
ft.Tab(label="A Only"),
ft.Tab(label="B Only"),
ft.Tab(label="Resolved"),
],
)
)

self.refresh_list(update_ui=False)

Expand All @@ -86,7 +103,10 @@ def setup_ui(self):
self.layout = ft.Row(
controls=[
ft.Container(
content=self.diff_list,
content=ft.Column([
self.filter_tabs,
self.diff_list
]),
width=350,
bgcolor=ft.Colors.SURFACE,
border_radius=10,
Expand All @@ -101,14 +121,45 @@ def setup_ui(self):
expand=True
)

def on_filter_change(self, e):
# In v0.84.0, e.data contains the index
idx = int(e.data) if e.data is not None else self.filter_tabs.selected_index
filters = ["all", "MODIFIED", "ONLY_IN_A", "ONLY_IN_B", "resolved"]
self.current_filter = filters[idx]
self.refresh_list()

def refresh_list(self, update_ui=True):
self.diff_list.controls.clear()
for diff in self.diff_results:

# Filter and Sort
filtered = []
for d in self.diff_results:
is_resolved = d.uuid in self.resolved_uuids
if self.current_filter == "all":
filtered.append(d)
elif self.current_filter == "resolved" and is_resolved:
filtered.append(d)
elif self.current_filter == d.state and not is_resolved:
filtered.append(d)

# Sort: Resolved at bottom, then by state, then by title
def sort_key(d):
is_resolved = d.uuid in self.resolved_uuids
# Resolved (1) or Not (0)
res_val = 1 if is_resolved else 0
# State priority: MODIFIED (0), ONLY_IN_A (1), ONLY_IN_B (2)
state_priority = {'MODIFIED': 0, 'ONLY_IN_A': 1, 'ONLY_IN_B': 2}.get(d.state, 3)
return (res_val, state_priority, d.title or "")

filtered.sort(key=sort_key)

for diff in filtered:
is_resolved = diff.uuid in self.resolved_uuids

icon = ft.Icons.QUESTION_MARK
color = ft.Colors.GREY
subtitle = ""
trailing = None

if is_resolved:
icon = ft.Icons.CHECK_CIRCLE
Expand All @@ -126,13 +177,30 @@ def refresh_list(self, update_ui=True):
icon = ft.Icons.EDIT
color = ft.Colors.ORANGE_400
subtitle = f"Changed: {', '.join(diff.diffs)}"

# Directional indicator
if diff.ahead == 'A':
trailing = ft.Container(
content=ft.Text("A NEWER", size=10, weight=ft.FontWeight.BOLD),
bgcolor=ft.Colors.INDIGO_700,
padding=ft.padding.symmetric(horizontal=8, vertical=4),
border_radius=4
)
elif diff.ahead == 'B':
trailing = ft.Container(
content=ft.Text("B NEWER", size=10, weight=ft.FontWeight.BOLD),
bgcolor=ft.Colors.TEAL_700,
padding=ft.padding.symmetric(horizontal=8, vertical=4),
border_radius=4
)

tile = ft.ListTile(
leading=ft.Icon(icon, color=color),
title=ft.Text(diff.title or "Untitled"),
subtitle=ft.Text(subtitle),
trailing=trailing,
on_click=lambda e, d=diff: self.show_details(d),
selected=False # Can track selection if needed
selected=False
)
self.diff_list.controls.append(tile)

Expand Down Expand Up @@ -184,18 +252,15 @@ def show_details(self, diff):
except:
ts_b = None

latest_is_a = False
latest_is_b = False
latest_is_a = (diff.ahead == 'A')
latest_is_b = (diff.ahead == 'B')

if ts_a and ts_b:
# If ahead is None (no history match and identical mtime), use mtime as fallback
if diff.ahead is None and ts_a and ts_b:
if ts_a > ts_b:
latest_is_a = True
elif ts_b > ts_a:
latest_is_b = True
elif ts_a and not ts_b:
latest_is_a = True
elif ts_b and not ts_a:
latest_is_b = True

def format_ts(ts):
if not ts: return "Unknown"
Expand Down