diff --git a/src/constants.py b/src/constants.py
new file mode 100644
index 0000000..9be50b8
--- /dev/null
+++ b/src/constants.py
@@ -0,0 +1,69 @@
+# constants.py
+#
+# Copyright 2025 Pavel Baksy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# 36 symbolic icons for projects (6x6 grid)
+PROJECT_ICONS = [
+ # Row 1: Folders & Organization
+ "folder-symbolic",
+ "folder-open-symbolic",
+ "folder-documents-symbolic",
+ "folder-download-symbolic",
+ "folder-music-symbolic",
+ "folder-pictures-symbolic",
+
+ # Row 2: Development & Code
+ "application-x-executable-symbolic",
+ "text-x-generic-symbolic",
+ "code-symbolic",
+ "utilities-terminal-symbolic",
+ "package-x-generic-symbolic",
+ "software-update-available-symbolic",
+
+ # Row 3: Network & Web
+ "network-server-symbolic",
+ "network-wireless-symbolic",
+ "weather-clear-symbolic",
+ "globe-symbolic",
+ "world-symbolic",
+ "web-browser-symbolic",
+
+ # Row 4: Tools & Actions
+ "document-edit-symbolic",
+ "preferences-system-symbolic",
+ "system-search-symbolic",
+ "view-list-symbolic",
+ "emblem-system-symbolic",
+ "edit-find-symbolic",
+
+ # Row 5: Objects & Concepts
+ "starred-symbolic",
+ "non-starred-symbolic",
+ "bookmark-new-symbolic",
+ "user-bookmarks-symbolic",
+ "tag-symbolic",
+ "mail-send-symbolic",
+
+ # Row 6: Status & Symbols
+ "emblem-ok-symbolic",
+ "dialog-warning-symbolic",
+ "dialog-information-symbolic",
+ "security-high-symbolic",
+ "changes-prevent-symbolic",
+ "document-properties-symbolic",
+]
diff --git a/src/icon-picker-dialog.ui b/src/icon-picker-dialog.ui
new file mode 100644
index 0000000..52fdc09
--- /dev/null
+++ b/src/icon-picker-dialog.ui
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+ Choose Icon
+ 400
+ 450
+
+
+
+
+
+
diff --git a/src/icon_picker_dialog.py b/src/icon_picker_dialog.py
new file mode 100644
index 0000000..aa6985a
--- /dev/null
+++ b/src/icon_picker_dialog.py
@@ -0,0 +1,70 @@
+# icon_picker_dialog.py
+#
+# Copyright 2025 Pavel Baksy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from gi.repository import Adw, Gtk, GObject
+from .constants import PROJECT_ICONS
+
+
+@Gtk.Template(resource_path='/cz/vesp/roster/icon-picker-dialog.ui')
+class IconPickerDialog(Adw.Dialog):
+ """Dialog for selecting a project icon."""
+
+ __gtype_name__ = 'IconPickerDialog'
+
+ icon_flowbox = Gtk.Template.Child()
+
+ __gsignals__ = {
+ 'icon-selected': (GObject.SIGNAL_RUN_FIRST, None, (str,)),
+ }
+
+ def __init__(self, current_icon=None):
+ super().__init__()
+ self.current_icon = current_icon
+ self._populate_icons()
+
+ def _populate_icons(self):
+ """Populate the flowbox with icon buttons."""
+ for icon_name in PROJECT_ICONS:
+ button = Gtk.Button()
+ button.set_icon_name(icon_name)
+ button.set_tooltip_text(icon_name)
+ button.add_css_class("flat")
+ button.set_size_request(48, 48)
+
+ # Create icon image with larger size
+ icon = Gtk.Image.new_from_icon_name(icon_name)
+ icon.set_icon_size(Gtk.IconSize.LARGE)
+ button.set_child(icon)
+
+ # Store icon name as data
+ button.icon_name = icon_name
+
+ # Highlight current icon
+ if icon_name == self.current_icon:
+ button.add_css_class("suggested-action")
+
+ self.icon_flowbox.append(button)
+
+ @Gtk.Template.Callback()
+ def on_icon_selected(self, flowbox, child):
+ """Handle icon selection."""
+ button = child.get_child()
+ if hasattr(button, 'icon_name'):
+ self.emit('icon-selected', button.icon_name)
+ self.close()
diff --git a/src/main-window.ui b/src/main-window.ui
index 328e34f..9bd7e57 100644
--- a/src/main-window.ui
+++ b/src/main-window.ui
@@ -83,69 +83,136 @@
-
- vertical
- 200
+
diff --git a/src/meson.build b/src/meson.build
index f0d2467..d02f448 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -34,6 +34,8 @@ roster_sources = [
'http_client.py',
'history_manager.py',
'project_manager.py',
+ 'constants.py',
+ 'icon_picker_dialog.py',
]
install_data(roster_sources, install_dir: moduledir)
@@ -45,6 +47,7 @@ widgets_sources = [
'widgets/history_item.py',
'widgets/project_item.py',
'widgets/request_item.py',
+ 'widgets/slim_project_item.py',
]
install_data(widgets_sources, install_dir: moduledir / 'widgets')
diff --git a/src/models.py b/src/models.py
index 6995d94..433a741 100644
--- a/src/models.py
+++ b/src/models.py
@@ -124,6 +124,7 @@ class Project:
name: str
requests: List[SavedRequest]
created_at: str # ISO format
+ icon: str = "folder-symbolic" # Icon name
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
@@ -131,7 +132,8 @@ class Project:
'id': self.id,
'name': self.name,
'requests': [req.to_dict() for req in self.requests],
- 'created_at': self.created_at
+ 'created_at': self.created_at,
+ 'icon': self.icon
}
@classmethod
@@ -141,5 +143,6 @@ class Project:
id=data['id'],
name=data['name'],
requests=[SavedRequest.from_dict(r) for r in data.get('requests', [])],
- created_at=data['created_at']
+ created_at=data['created_at'],
+ icon=data.get('icon', 'folder-symbolic') # Default for old data
)
diff --git a/src/project_manager.py b/src/project_manager.py
index 4503267..7bcb1b5 100644
--- a/src/project_manager.py
+++ b/src/project_manager.py
@@ -76,12 +76,15 @@ class ProjectManager:
self.save_projects(projects)
return project
- def rename_project(self, project_id: str, new_name: str):
- """Rename a project."""
+ def update_project(self, project_id: str, new_name: str = None, new_icon: str = None):
+ """Update a project's name and/or icon."""
projects = self.load_projects()
for p in projects:
if p.id == project_id:
- p.name = new_name
+ if new_name is not None:
+ p.name = new_name
+ if new_icon is not None:
+ p.icon = new_icon
break
self.save_projects(projects)
diff --git a/src/roster.gresource.xml b/src/roster.gresource.xml
index 2da70e7..e141c03 100644
--- a/src/roster.gresource.xml
+++ b/src/roster.gresource.xml
@@ -3,9 +3,11 @@
main-window.ui
shortcuts-dialog.ui
+ icon-picker-dialog.ui
widgets/header-row.ui
widgets/history-item.ui
widgets/project-item.ui
widgets/request-item.ui
+ widgets/slim-project-item.ui
diff --git a/src/widgets/__init__.py b/src/widgets/__init__.py
index afc6ef2..c5be70a 100644
--- a/src/widgets/__init__.py
+++ b/src/widgets/__init__.py
@@ -21,5 +21,6 @@ from .header_row import HeaderRow
from .history_item import HistoryItem
from .project_item import ProjectItem
from .request_item import RequestItem
+from .slim_project_item import SlimProjectItem
-__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem']
+__all__ = ['HeaderRow', 'HistoryItem', 'ProjectItem', 'RequestItem', 'SlimProjectItem']
diff --git a/src/widgets/project-item.ui b/src/widgets/project-item.ui
index ada6ec1..adbd0c5 100644
--- a/src/widgets/project-item.ui
+++ b/src/widgets/project-item.ui
@@ -10,8 +10,8 @@
project.add-request
-
- Rename
- project.rename
+ Edit Project
+ project.edit
-
Delete
@@ -39,6 +39,12 @@
+
+
+ folder-symbolic
+
+
+
0
diff --git a/src/widgets/project_item.py b/src/widgets/project_item.py
index 5d2a362..fac522b 100644
--- a/src/widgets/project_item.py
+++ b/src/widgets/project_item.py
@@ -28,12 +28,13 @@ class ProjectItem(Gtk.Box):
__gtype_name__ = 'ProjectItem'
expand_icon = Gtk.Template.Child()
+ project_icon = Gtk.Template.Child()
name_label = Gtk.Template.Child()
requests_revealer = Gtk.Template.Child()
requests_listbox = Gtk.Template.Child()
__gsignals__ = {
- 'rename-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
+ 'edit-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'delete-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'add-request-requested': (GObject.SIGNAL_RUN_FIRST, None, ()),
'request-load-requested': (GObject.SIGNAL_RUN_FIRST, None, (object,)),
@@ -60,8 +61,8 @@ class ProjectItem(Gtk.Box):
action.connect("activate", lambda *_: self.emit('add-request-requested'))
actions.add_action(action)
- action = Gio.SimpleAction.new("rename", None)
- action.connect("activate", lambda *_: self.emit('rename-requested'))
+ action = Gio.SimpleAction.new("edit", None)
+ action.connect("activate", lambda *_: self.emit('edit-requested'))
actions.add_action(action)
action = Gio.SimpleAction.new("delete", None)
@@ -73,6 +74,7 @@ class ProjectItem(Gtk.Box):
def _populate(self):
"""Populate with project data."""
self.name_label.set_text(self.project.name)
+ self.project_icon.set_from_icon_name(self.project.icon)
# Clear and add requests
while child := self.requests_listbox.get_first_child():
diff --git a/src/widgets/slim-project-item.ui b/src/widgets/slim-project-item.ui
new file mode 100644
index 0000000..7a8964d
--- /dev/null
+++ b/src/widgets/slim-project-item.ui
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Project
+ 48
+ 48
+
+
+
+
+
+ folder-symbolic
+ large
+
+
+
+
diff --git a/src/widgets/slim_project_item.py b/src/widgets/slim_project_item.py
new file mode 100644
index 0000000..f5b3e2e
--- /dev/null
+++ b/src/widgets/slim_project_item.py
@@ -0,0 +1,44 @@
+# slim_project_item.py
+#
+# Copyright 2025 Pavel Baksy
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from gi.repository import Gtk, GObject
+
+
+@Gtk.Template(resource_path='/cz/vesp/roster/widgets/slim-project-item.ui')
+class SlimProjectItem(Gtk.Button):
+ """Slim widget showing only project icon (for folded sidebar)."""
+
+ __gtype_name__ = 'SlimProjectItem'
+
+ project_icon = Gtk.Template.Child()
+
+ __gsignals__ = {
+ 'project-clicked': (GObject.SIGNAL_RUN_FIRST, None, ()),
+ }
+
+ def __init__(self, project):
+ super().__init__()
+ self.project = project
+ self.project_icon.set_from_icon_name(project.icon)
+ self.set_tooltip_text(project.name)
+
+ @Gtk.Template.Callback()
+ def on_clicked(self, button):
+ """Handle click - unfold sidebar and show this project."""
+ self.emit('project-clicked')
diff --git a/src/window.py b/src/window.py
index 777ebc1..75935f9 100644
--- a/src/window.py
+++ b/src/window.py
@@ -22,9 +22,11 @@ from .models import HttpRequest, HttpResponse, HistoryEntry
from .http_client import HttpClient
from .history_manager import HistoryManager
from .project_manager import ProjectManager
+from .icon_picker_dialog import IconPickerDialog
from .widgets.header_row import HeaderRow
from .widgets.history_item import HistoryItem
from .widgets.project_item import ProjectItem
+from .widgets.slim_project_item import SlimProjectItem
from datetime import datetime
import threading
@@ -43,9 +45,13 @@ class RosterWindow(Adw.ApplicationWindow):
split_pane = Gtk.Template.Child()
# Sidebar widgets
+ sidebar_stack = Gtk.Template.Child()
projects_listbox = Gtk.Template.Child()
+ slim_projects_box = Gtk.Template.Child()
add_project_button = Gtk.Template.Child()
save_request_button = Gtk.Template.Child()
+ fold_sidebar_button = Gtk.Template.Child()
+ unfold_sidebar_button = Gtk.Template.Child()
# Containers for tabs
request_tabs_container = Gtk.Template.Child()
@@ -452,18 +458,26 @@ class RosterWindow(Adw.ApplicationWindow):
# Clear existing
while child := self.projects_listbox.get_first_child():
self.projects_listbox.remove(child)
+ while child := self.slim_projects_box.get_first_child():
+ self.slim_projects_box.remove(child)
# Load and populate
projects = self.project_manager.load_projects()
for project in projects:
+ # Full project item
item = ProjectItem(project)
- item.connect('rename-requested', self._on_project_rename, project)
+ item.connect('edit-requested', self._on_project_edit, project)
item.connect('delete-requested', self._on_project_delete, project)
item.connect('add-request-requested', self._on_add_to_project, project)
item.connect('request-load-requested', self._on_load_request)
item.connect('request-delete-requested', self._on_delete_request, project)
self.projects_listbox.append(item)
+ # Slim project item
+ slim_item = SlimProjectItem(project)
+ slim_item.connect('project-clicked', self._on_slim_project_clicked)
+ self.slim_projects_box.append(slim_item)
+
@Gtk.Template.Callback()
def on_add_project_clicked(self, button):
"""Show dialog to create project."""
@@ -491,25 +505,53 @@ class RosterWindow(Adw.ApplicationWindow):
dialog.connect("response", on_response)
dialog.present(self)
- def _on_project_rename(self, widget, project):
- """Show rename dialog."""
+ def _on_project_edit(self, widget, project):
+ """Show edit project dialog with icon picker."""
dialog = Adw.AlertDialog()
- dialog.set_heading("Rename Project")
- dialog.set_body(f"Rename '{project.name}' to:")
+ dialog.set_heading("Edit Project")
+ dialog.set_body(f"Edit '{project.name}':")
+ box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12)
+
+ # Project name entry
+ name_label = Gtk.Label(label="Name:", xalign=0)
entry = Gtk.Entry()
entry.set_text(project.name)
- dialog.set_extra_child(entry)
+ box.append(name_label)
+ box.append(entry)
+
+ # Icon selector button
+ icon_label = Gtk.Label(label="Icon:", xalign=0)
+ icon_button = Gtk.Button()
+ icon_button.set_child(Gtk.Image.new_from_icon_name(project.icon))
+ icon_button.selected_icon = project.icon # Store current selection
+
+ def on_icon_button_clicked(btn):
+ icon_dialog = IconPickerDialog(current_icon=btn.selected_icon)
+
+ def on_icon_selected(dlg, icon_name):
+ btn.selected_icon = icon_name
+ btn.set_child(Gtk.Image.new_from_icon_name(icon_name))
+
+ icon_dialog.connect('icon-selected', on_icon_selected)
+ icon_dialog.present(self)
+
+ icon_button.connect('clicked', on_icon_button_clicked)
+ box.append(icon_label)
+ box.append(icon_button)
+
+ dialog.set_extra_child(box)
dialog.add_response("cancel", "Cancel")
- dialog.add_response("rename", "Rename")
- dialog.set_response_appearance("rename", Adw.ResponseAppearance.SUGGESTED)
+ dialog.add_response("save", "Save")
+ dialog.set_response_appearance("save", Adw.ResponseAppearance.SUGGESTED)
def on_response(dlg, response):
- if response == "rename":
+ if response == "save":
new_name = entry.get_text().strip()
+ new_icon = icon_button.selected_icon
if new_name:
- self.project_manager.rename_project(project.id, new_name)
+ self.project_manager.update_project(project.id, new_name, new_icon)
self._load_projects()
dialog.connect("response", on_response)
@@ -667,3 +709,19 @@ class RosterWindow(Adw.ApplicationWindow):
dialog.connect("response", on_response)
dialog.present(self)
+
+ # Sidebar fold/unfold methods
+
+ @Gtk.Template.Callback()
+ def on_fold_sidebar_clicked(self, button):
+ """Fold the sidebar to slim view."""
+ self.sidebar_stack.set_visible_child_name("slim")
+
+ @Gtk.Template.Callback()
+ def on_unfold_sidebar_clicked(self, button):
+ """Unfold the sidebar to full view."""
+ self.sidebar_stack.set_visible_child_name("full")
+
+ def _on_slim_project_clicked(self, slim_item):
+ """Handle click on slim project item - unfold sidebar."""
+ self.sidebar_stack.set_visible_child_name("full")