Channel context menu: long-press/right-click a row → rename, favorite/unfavorite, add/change/remove icon (color or image), delete; removed star/delete row buttons; favorite + icon/color synced across devices

This commit is contained in:
ARIA committed 2026-08-20 20:55:24 +02:00
1 parent 0d99ee5cf5
commit b681e547e7
12 files changed
+574 -68

No files matched your search

+53
View File
@@ -2100,6 +2100,59 @@ class AndroidAdapter(BasePlatformAdapter):
resp.id = frame.id
await self._ws_server.broadcast(resp)
async def on_channel_favorite(self, frame: protocol.Frame, device_id: str) -> None:
chat_id = frame.chat_id or frame.payload.get("chat_id")
if not isinstance(chat_id, str) or not chat_id.strip():
await self._ws_server.send_to(
device_id,
protocol.error(protocol.ERR_NOT_FOUND, "channel.favorite requires chat_id", id=frame.id),
)
return
on = bool(frame.payload.get("on"))
entry = self._channels.set_favorite(chat_id, on)
if entry is None:
await self._ws_server.send_to(
device_id,
protocol.error(protocol.ERR_NOT_FOUND, f"unknown chat_id {chat_id}", id=frame.id),
)
return
# Reuse the renamed event shape: it carries the full entry (incl. the
# new favorite flag) so every device reconciles the change.
resp = protocol.channel_renamed(entry)
resp.id = frame.id
await self._ws_server.broadcast(resp)
async def on_channel_icon(self, frame: protocol.Frame, device_id: str) -> None:
chat_id = frame.chat_id or frame.payload.get("chat_id")
if not isinstance(chat_id, str) or not chat_id.strip():
await self._ws_server.send_to(
device_id,
protocol.error(protocol.ERR_NOT_FOUND, "channel.icon requires chat_id", id=frame.id),
)
return
payload = frame.payload
icon = payload.get("icon")
icon = icon if isinstance(icon, str) and icon else None
color = payload.get("color")
color = color if isinstance(color, str) and color else None
# Guard against a runaway base64 blob (a channel icon is small).
if icon is not None and len(icon) > 512 * 1024:
await self._ws_server.send_to(
device_id,
protocol.error(protocol.ERR_UNSUPPORTED, "channel icon too large", id=frame.id),
)
return
entry = self._channels.set_icon(chat_id, icon, color)
if entry is None:
await self._ws_server.send_to(
device_id,
protocol.error(protocol.ERR_NOT_FOUND, f"unknown chat_id {chat_id}", id=frame.id),
)
return
resp = protocol.channel_renamed(entry)
resp.id = frame.id
await self._ws_server.broadcast(resp)
async def on_channel_delete(self, frame: protocol.Frame, device_id: str) -> None:
chat_id = frame.chat_id or frame.payload.get("chat_id")
if not isinstance(chat_id, str) or not chat_id.strip():
+50 -3
View File
@@ -64,10 +64,25 @@ class ChannelDirectory:
parent_chat_id TEXT,
is_default INTEGER NOT NULL DEFAULT 0,
archived INTEGER NOT NULL DEFAULT 0,
created REAL NOT NULL DEFAULT 0
created REAL NOT NULL DEFAULT 0,
favorite INTEGER NOT NULL DEFAULT 0,
icon TEXT,
color TEXT
)
"""
)
# Migrate existing DBs: add the cosmetic columns if missing.
existing = {
row[1] for row in self._conn.execute("PRAGMA table_info(channels)")
}
if "favorite" not in existing:
self._conn.execute(
"ALTER TABLE channels ADD COLUMN favorite INTEGER NOT NULL DEFAULT 0"
)
if "icon" not in existing:
self._conn.execute("ALTER TABLE channels ADD COLUMN icon TEXT")
if "color" not in existing:
self._conn.execute("ALTER TABLE channels ADD COLUMN color TEXT")
self._conn.execute(
"""
CREATE TABLE IF NOT EXISTS counters (
@@ -206,6 +221,35 @@ class ChannelDirectory:
self._conn.commit()
return self.get(chat_id)
def set_favorite(self, chat_id: str, on: bool) -> Optional[Dict[str, Any]]:
"""Toggle the cosmetic favorite flag (sorts to the top of the list)."""
with self._lock:
cur = self._conn.execute(
"UPDATE channels SET favorite = ? WHERE chat_id = ? AND archived = 0",
(1 if on else 0, chat_id),
)
self._conn.commit()
if cur.rowcount == 0:
return None
return self.get(chat_id)
def set_icon(self, chat_id: str, icon: Optional[str], color: Optional[str]) -> Optional[Dict[str, Any]]:
"""Set the channel's cosmetic icon (base64 image) and/or avatar color.
``icon`` is a base64-encoded image (or ``None`` to clear it); ``color``
is a ``#RRGGBB`` hex string (or ``None`` to clear it). Both are purely
cosmetic and independent of the name / default flag.
"""
with self._lock:
cur = self._conn.execute(
"UPDATE channels SET icon = ?, color = ? WHERE chat_id = ? AND archived = 0",
(icon, color, chat_id),
)
self._conn.commit()
if cur.rowcount == 0:
return None
return self.get(chat_id)
def delete(self, chat_id: str) -> Optional[Dict[str, Any]]:
"""Soft-delete (archive) a channel. History stays for search.
@@ -234,11 +278,11 @@ class ChannelDirectory:
return _row_to_entry(row) if row else None
def list(self, include_archived: bool = False) -> List[Dict[str, Any]]:
"""Directory listing. Default channel first, then creation order."""
"""Directory listing. Default first, then favorites, then creation order."""
sql = "SELECT * FROM channels"
if not include_archived:
sql += " WHERE archived = 0"
sql += " ORDER BY is_default DESC, created ASC"
sql += " ORDER BY is_default DESC, favorite DESC, created ASC"
with self._lock:
rows = self._conn.execute(sql).fetchall()
return [_row_to_entry(r) for r in rows]
@@ -318,6 +362,9 @@ def _row_to_entry(row: sqlite3.Row) -> Dict[str, Any]:
"is_default": bool(row["is_default"]),
"archived": bool(row["archived"]),
"created": row["created"],
"favorite": bool(row["favorite"]),
"icon": row["icon"],
"color": row["color"],
}
+8
View File
@@ -58,6 +58,8 @@ TYPE_COMMENTARY = "commentary"
TYPE_CHANNEL_CREATE = "channel.create"
TYPE_CHANNEL_RENAME = "channel.rename"
TYPE_CHANNEL_SET_DEFAULT = "channel.set_default"
TYPE_CHANNEL_FAVORITE = "channel.favorite"
TYPE_CHANNEL_ICON = "channel.icon"
TYPE_CHANNEL_DELETE = "channel.delete"
TYPE_CHANNEL_CREATED = "channel.created"
TYPE_CHANNEL_RENAMED = "channel.renamed"
@@ -434,6 +436,12 @@ def _channel_payload(entry: Dict[str, Any]) -> Dict[str, Any]:
payload["is_default"] = True
if entry.get("archived"):
payload["archived"] = True
if entry.get("favorite"):
payload["favorite"] = True
if entry.get("icon"):
payload["icon"] = entry["icon"]
if entry.get("color"):
payload["color"] = entry["color"]
return payload
+4
View File
@@ -370,6 +370,10 @@ class WsServer:
await self._adapter.on_channel_rename(frame, device_id)
elif frame.type == protocol.TYPE_CHANNEL_SET_DEFAULT:
await self._adapter.on_channel_set_default(frame, device_id)
elif frame.type == protocol.TYPE_CHANNEL_FAVORITE:
await self._adapter.on_channel_favorite(frame, device_id)
elif frame.type == protocol.TYPE_CHANNEL_ICON:
await self._adapter.on_channel_icon(frame, device_id)
elif frame.type == protocol.TYPE_CHANNEL_DELETE:
await self._adapter.on_channel_delete(frame, device_id)
elif frame.type == protocol.TYPE_CHANNEL_LIST: