Skip to content

Commit 4f4ae9c

Browse files
feat: add sorting options
1 parent 42dc3c5 commit 4f4ae9c

File tree

10 files changed

+866
-121
lines changed

10 files changed

+866
-121
lines changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,16 @@ A cross-platform desktop application for viewing random images from folders or c
2323
- [🛠️ Advanced Features](#️-advanced-features)
2424
- [🏗️ Technical Details](#️-technical-details)
2525
- [🎨 Screenshots](#-screenshots)
26+
- [🙏 Inspiration](#-inspiration)
2627
- [🤝 Contributing](#-contributing)
2728
- [📝 License](#-license)
2829

2930
## ✨ Features
3031

31-
### 🎲 **Collections & Random Viewing**
32+
### 🎲 **Collections & Smart Sorting**
3233
- Organize multiple folders into collections
34+
- **Flexible sorting options**: Random (shuffle), Alphabetical (name), Full path, File size, Date modified
35+
- **Ascending/descending order** for all sort methods except random
3336
- Quick shuffle mode for single folders
3437
- History navigation with thumbnail panel
3538

@@ -149,6 +152,8 @@ Alternatively, use "Quick Shuffle Folder" to browse any folder immediately.
149152

150153
### Collection Management
151154
- Professional startup dialog with ShuffleBird-inspired design
155+
- **Smart sorting system**: Choose between random shuffle or organized viewing (name, path, size, date)
156+
- **Flexible sort orders**: Ascending/descending options for structured browsing
152157
- Edit collections (rename, manage folders)
153158
- Delete collections with confirmation
154159
- Auto-sorted by recent usage
@@ -209,6 +214,20 @@ Alternatively, use "Quick Shuffle Folder" to browse any folder immediately.
209214

210215
---
211216

217+
## 🙏 Inspiration
218+
219+
This project was inspired by and references several excellent applications:
220+
221+
### Primary Inspirations
222+
- **[ShuffleBird](https://github.com/AvantinumCode/ShuffleBird)** - Random image viewer with clean interface design. Glimpse's startup dialog and professional UI aesthetic draw inspiration from ShuffleBird's polished approach to image collection management.
223+
224+
- **[GestureSesh](https://github.com/AvantinumCode/GestureSesh)** - Figure drawing application with timer functionality. The timer system and media-style controls in Glimpse were influenced by GestureSesh's focus on timed practice sessions.
225+
226+
### Design Philosophy
227+
We believe in building upon the excellent work of the open source community. These projects provided valuable insights into user experience design for image viewing applications and helped shape Glimpse's approach to collection management and viewing workflows.
228+
229+
---
230+
212231
## 🤝 Contributing
213232

214233
This project was built collaboratively with Claude Code. We welcome:

TODO.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
## Code refactoring
2+
- [ ] refactor code so it will be easier to maintain
3+
14
## Build and releasing
25
- [x] update keyboard shortcuts; add more if needed
36
- [x] update screenshots with all the new UI
4-
- [ ] setup github pipelines so we can create builds and releases and versioning and changelog. make it all automatic using semantic versioning. we need to build for windows, linux and mac if possible.
7+
- [x] setup github pipelines so we can create builds and releases and versioning and changelog. make it all automatic using semantic versioning. we need to build for windows, linux and mac if possible.
58

69
## Phase 1: Foundation & Code Quality
710
- [x] Refactor the main.py and make it modular - easier to maintain

icons/sort_asc.svg

Lines changed: 5 additions & 0 deletions
Loading

icons/sort_desc.svg

Lines changed: 5 additions & 0 deletions
Loading

src/core/collections.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import json
5+
import re
56
from datetime import datetime
67
from typing import List, Dict, Optional
78
from PySide6.QtCore import QStandardPaths
@@ -13,12 +14,15 @@ class Collection:
1314
"""Represents a collection of image folders."""
1415

1516
def __init__(self, name: str, paths: List[str], created_date: Optional[str] = None,
16-
last_used: Optional[str] = None, image_count: int = 0):
17+
last_used: Optional[str] = None, image_count: int = 0,
18+
sort_method: str = "random", sort_descending: bool = False):
1719
self.name = name
1820
self.paths = paths
1921
self.created_date = created_date or datetime.now().isoformat()
2022
self.last_used = last_used
2123
self.image_count = image_count
24+
self.sort_method = sort_method # "random", "name", "path", "size", "date"
25+
self.sort_descending = sort_descending
2226

2327
def to_dict(self) -> Dict:
2428
"""Convert collection to dictionary for JSON serialization."""
@@ -27,7 +31,9 @@ def to_dict(self) -> Dict:
2731
'paths': self.paths,
2832
'created_date': self.created_date,
2933
'last_used': self.last_used,
30-
'image_count': self.image_count
34+
'image_count': self.image_count,
35+
'sort_method': self.sort_method,
36+
'sort_descending': self.sort_descending
3137
}
3238

3339
@classmethod
@@ -38,7 +44,9 @@ def from_dict(cls, data: Dict) -> 'Collection':
3844
paths=data['paths'],
3945
created_date=data.get('created_date'),
4046
last_used=data.get('last_used'),
41-
image_count=data.get('image_count', 0)
47+
image_count=data.get('image_count', 0),
48+
sort_method=data.get('sort_method', 'random'),
49+
sort_descending=data.get('sort_descending', False)
4250
)
4351

4452
def get_all_images(self) -> List[str]:
@@ -49,6 +57,68 @@ def get_all_images(self) -> List[str]:
4957
all_images.extend(get_images_in_folder(path))
5058
return all_images
5159

60+
def get_sorted_images(self) -> List[str]:
61+
"""Get all images sorted according to the collection's sort method."""
62+
images = self.get_all_images()
63+
64+
if self.sort_method == "random":
65+
import random
66+
random.shuffle(images)
67+
elif self.sort_method == "name":
68+
def natural_sort_key(path):
69+
"""Generate a key for natural/human sorting of filenames.
70+
71+
Converts 'image1.jpg', 'image10.jpg', 'image2.jpg' to sort as:
72+
'image1.jpg', 'image2.jpg', 'image10.jpg'
73+
"""
74+
name = os.path.basename(path).lower()
75+
# Split the filename into text and number parts
76+
parts = re.split(r'(\d+)', name)
77+
# Convert numeric parts to integers for proper sorting
78+
result = []
79+
for part in parts:
80+
if part.isdigit():
81+
result.append(int(part))
82+
else:
83+
result.append(part)
84+
return result
85+
86+
images.sort(key=natural_sort_key, reverse=self.sort_descending)
87+
elif self.sort_method == "path":
88+
def natural_sort_key_path(path):
89+
"""Generate a key for natural/human sorting of full paths."""
90+
name = path.lower()
91+
# Split the path into text and number parts
92+
parts = re.split(r'(\d+)', name)
93+
# Convert numeric parts to integers for proper sorting
94+
result = []
95+
for part in parts:
96+
if part.isdigit():
97+
result.append(int(part))
98+
else:
99+
result.append(part)
100+
return result
101+
102+
images.sort(key=natural_sort_key_path, reverse=self.sort_descending)
103+
elif self.sort_method == "size":
104+
# Sort by file size, handling missing files gracefully
105+
def get_size(path):
106+
try:
107+
return os.path.getsize(path)
108+
except (OSError, FileNotFoundError):
109+
return 0
110+
images.sort(key=get_size, reverse=self.sort_descending)
111+
elif self.sort_method == "date":
112+
# Sort by modification date, handling missing files gracefully
113+
def get_mtime(path):
114+
try:
115+
return os.path.getmtime(path)
116+
except (OSError, FileNotFoundError):
117+
return 0
118+
images.sort(key=get_mtime, reverse=self.sort_descending)
119+
120+
return images
121+
52122
def update_image_count(self):
53123
"""Update the cached image count."""
54124
self.image_count = len(self.get_all_images())
@@ -142,12 +212,13 @@ def collection_exists(self, collection_name: str) -> bool:
142212
file_path = self._get_collection_file_path(collection_name)
143213
return os.path.exists(file_path)
144214

145-
def create_collection(self, name: str, paths: List[str]) -> Optional[Collection]:
146-
"""Create a new collection with the given paths."""
215+
def create_collection(self, name: str, paths: List[str], sort_method: str = "random",
216+
sort_descending: bool = False) -> Optional[Collection]:
217+
"""Create a new collection with the given paths and sorting options."""
147218
if self.collection_exists(name):
148219
return None # Collection already exists
149220

150-
collection = Collection(name, paths)
221+
collection = Collection(name, paths, sort_method=sort_method, sort_descending=sort_descending)
151222
collection.update_image_count() # Calculate initial image count
152223

153224
if self.save_collection(collection):

src/core/image_utils.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,5 +450,63 @@ def _create_coded_icon(icon_type, size=24, color="#ffffff"):
450450
tab_y = folder_y - tab_height // 2
451451
painter.drawRoundedRect(margin, tab_y, tab_width, tab_height, 1, 1)
452452

453+
elif icon_type == "sort_asc":
454+
# Sort ascending icon (triangle pointing up with lines)
455+
painter.setBrush(Qt.NoBrush)
456+
painter.setPen(pen)
457+
458+
# Three horizontal lines getting shorter going up (representing sorted data)
459+
line_spacing = size // 6
460+
base_width = size - 2 * margin
461+
462+
for i in range(3):
463+
y = margin + size // 4 + i * line_spacing
464+
width = base_width - (i * base_width // 4) # Each line gets shorter
465+
x = margin + (base_width - width) // 2 # Center the line
466+
painter.drawLine(x, y, x + width, y)
467+
468+
# Small triangle pointing up (indicating ascending direction)
469+
triangle_size = size // 6
470+
triangle_x = center
471+
triangle_y = margin + size // 8
472+
473+
points = [
474+
QPoint(triangle_x, triangle_y), # Top point
475+
QPoint(triangle_x - triangle_size//2, triangle_y + triangle_size), # Bottom left
476+
QPoint(triangle_x + triangle_size//2, triangle_y + triangle_size) # Bottom right
477+
]
478+
painter.setBrush(brush)
479+
painter.setPen(Qt.NoPen)
480+
painter.drawPolygon(QPolygon(points))
481+
482+
elif icon_type == "sort_desc":
483+
# Sort descending icon (triangle pointing down with lines)
484+
painter.setBrush(Qt.NoBrush)
485+
painter.setPen(pen)
486+
487+
# Three horizontal lines getting longer going down (representing sorted data)
488+
line_spacing = size // 6
489+
base_width = size - 2 * margin
490+
491+
for i in range(3):
492+
y = margin + size // 4 + i * line_spacing
493+
width = base_width - ((2-i) * base_width // 4) # Each line gets longer
494+
x = margin + (base_width - width) // 2 # Center the line
495+
painter.drawLine(x, y, x + width, y)
496+
497+
# Small triangle pointing down (indicating descending direction)
498+
triangle_size = size // 6
499+
triangle_x = center
500+
triangle_y = size - margin - size // 8
501+
502+
points = [
503+
QPoint(triangle_x - triangle_size//2, triangle_y - triangle_size), # Top left
504+
QPoint(triangle_x + triangle_size//2, triangle_y - triangle_size), # Top right
505+
QPoint(triangle_x, triangle_y) # Bottom point
506+
]
507+
painter.setBrush(brush)
508+
painter.setPen(Qt.NoPen)
509+
painter.drawPolygon(QPolygon(points))
510+
453511
painter.end()
454512
return QIcon(pixmap)

0 commit comments

Comments
 (0)