Skip to content
Merged
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
43 changes: 43 additions & 0 deletions faststack/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# ChangeLog

## Version 0.3

### New Features
- Implemented a "Settings" dialog with the following configurable options:
- Helicon Focus executable path (with validation).
- Image cache size (in GB).
- Image prefetch radius.
- Application theme (Dark/Light).
- Default image directory.

## Version 0.2

### New Features
- Added an "Actions" menu with the following options:
- "Run Stacks": Launch Helicon Focus with selected files or all stacks.
- "Clear Stacks": Clear all defined stacks.
- "Show Stacks": Display a dialog with information about the defined stacks.
- Pressing the 'S' key now adds or removes a RAW file from the selection for processing.
- Implemented tracking for stacked images:
- `EntryMetadata` now includes `stacked` (boolean) and `stacked_date` (string) fields.
- `launch_helicon` records stacking status and date upon successful launch.
- The footer in `Main.qml` displays "Stacked: [date]" for previously stacked images.

### Changes
- Pressing the 'Enter' key will now launch Helicon Focus with the selected RAW files. If no files are selected, it will launch with all defined stacks.
- Refactored the theme toggling logic in `Main.qml` to use a boolean `isDarkTheme` property for more robustness.

### Bug Fixes
- Fixed an issue where both the main "Enter" key and the numeric keypad "Enter" key were not consistently recognized.
- The "Show Stacks" and "Key Bindings" dialogs now correctly follow the application's theme (light/dark mode).
- Fixed a bug that caused the "Show Stacks" dialog to be blank.
- Resolved a `NameError` caused by using `Optional` without importing it.
- Corrected an import error for `EntryMetadata` in the tests.
- Updated a test to assert the correct default version number.
- Fixed a `TypeError` in tests caused by a missing `stack_id` field in the `EntryMetadata` model.
- Resolved a QML issue where `anchors.fill` conflicted with manual positioning, preventing panning and zooming.
- Corrected the `launch_helicon` method to only clear the `selected_raws` set if Helicon Focus is launched successfully.
- Resolved `TypeError` and `Invalid property assignment` errors in QML related to settings dialog initialization and property bindings.
- Fixed QML warnings related to invalid anchor usage in `Main.qml`.
- Fixed missing minimize, maximize, and close buttons by correctly configuring the custom title bar.
- Resolved QML warnings about `mouse` parameter not being declared in `MouseArea` signal handlers.
4 changes: 2 additions & 2 deletions faststack/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# FastStack

# Version 0.1 - October 31, 2025
# Version 0.3 - November 1, 2025
# By Alan Rockefeller

Ultra-fast, caching JPG viewer designed for culling and selecting RAW files for focus stacking.
Expand Down Expand Up @@ -35,7 +35,7 @@ This tool is optimized for speed, using `libjpeg-turbo` for decoding, aggressive
- `J` / `Right Arrow`: Next Image
- `K` / `Left Arrow`: Previous Image
- `G`: Toggle Grid View
- `S`: Stack all of the selected stacks with Helicon Focus
- `S`: Toggle selection of current image for stacking
- `[`: Begin new stack group
- `]`: End current stack group
- `Space`: Toggle Flag
Expand Down
101 changes: 91 additions & 10 deletions faststack/faststack/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import sys
from pathlib import Path
from typing import Optional, List, Dict
from datetime import date

import os
import typer
import concurrent.futures
from PySide6.QtCore import QUrl, QTimer, QObject, QEvent
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import QApplication, QFileDialog
from PySide6.QtQml import QQmlApplicationEngine

from faststack.config import config
Expand Down Expand Up @@ -41,7 +42,8 @@ def __init__(self, image_dir: Path, engine: QQmlApplicationEngine):
self.sidecar = SidecarManager(self.image_dir, self.watcher)

# -- Caching & Prefetching --
cache_size_bytes = config.getint('core', 'cache_bytes', int(1.5 * 1024**3))
cache_size_gb = config.getfloat('core', 'cache_size_gb', 1.5)
cache_size_bytes = int(cache_size_gb * 1024**3)
self.image_cache = ByteLRUCache(max_bytes=cache_size_bytes, size_of=get_decoded_image_size)
self.prefetcher = Prefetcher(
image_files=self.image_files,
Expand Down Expand Up @@ -77,6 +79,9 @@ def load(self):
self.prefetcher.update_prefetch(self.current_index)
self.sync_ui_state()

theme = config.get('core', 'theme')
self.main_window.setProperty('isDarkTheme', theme == 'dark')

def refresh_image_list(self):
"""Rescans the directory for images."""
self.image_files = find_images(self.image_dir)
Expand Down Expand Up @@ -153,6 +158,8 @@ def get_current_metadata(self) -> Dict:
"flag": meta.flag,
"reject": meta.reject,
"stack_info_text": stack_info,
"stacked": meta.stacked,
"stacked_date": meta.stacked_date,
}

def toggle_current_flag(self):
Expand Down Expand Up @@ -216,7 +223,7 @@ def launch_helicon(self):
raw_files_to_process.extend(sorted(list(self.selected_raws))) # Sort for consistent order
elif self.stacks:
log.info("No selection, launching Helicon with all defined stacks.")
for i, (start, end) in enumerate(self.stacks):
for start, end in self.stacks:
for idx in range(start, end + 1):
if idx < len(self.image_files) and self.image_files[idx].raw_pair:
raw_files_to_process.append(self.image_files[idx].raw_pair)
Expand All @@ -232,10 +239,23 @@ def launch_helicon(self):
if success and tmp_path:
# Schedule delayed deletion of the temporary file
QTimer.singleShot(5000, lambda: self._delete_temp_file(tmp_path))

# Clear selection after launching
self.selected_raws.clear()
self.sync_ui_state()

# Record stacking metadata
today = date.today().isoformat()
for raw_path in unique_raw_files:
# Find the corresponding image file to get the stem
for img_file in self.image_files:
if img_file.raw_pair == raw_path:
stem = img_file.path.stem
meta = self.sidecar.get_metadata(stem)
meta.stacked = True
meta.stacked_date = today
break
self.sidecar.save()

# Clear selection after launching
self.selected_raws.clear()
self.sync_ui_state()
else:
log.warning("No valid RAW files found to launch Helicon.")

Expand All @@ -254,12 +274,65 @@ def clear_all_stacks(self):
self.sidecar.save()
self.ui_state.metadataChanged.emit() # Refresh UI to show no stacks

def get_helicon_path(self):
return config.get('helicon', 'exe')

def set_helicon_path(self, path):
config.set('helicon', 'exe', path)
config.save()

def open_file_dialog(self):
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
dialog.setNameFilter("Executables (*.exe)")
if dialog.exec():
return dialog.selectedFiles()[0]
return ""

def check_path_exists(self, path):
return os.path.exists(path)

def get_cache_size(self):
return config.getfloat('core', 'cache_size_gb')

def set_cache_size(self, size):
config.set('core', 'cache_size_gb', size)
config.save()

def get_prefetch_radius(self):
return config.getint('core', 'prefetch_radius')

def set_prefetch_radius(self, radius):
config.set('core', 'prefetch_radius', radius)
config.save()

def get_theme(self):
return 0 if config.get('core', 'theme') == 'dark' else 1

def set_theme(self, theme_index):
theme = 'dark' if theme_index == 0 else 'light'
config.set('core', 'theme', theme)
config.save()

def get_default_directory(self):
return config.get('core', 'default_directory')

def set_default_directory(self, path):
config.set('core', 'default_directory', path)
config.save()

def open_directory_dialog(self):
dialog = QFileDialog()
dialog.setFileMode(QFileDialog.FileMode.Directory)
if dialog.exec():
return dialog.selectedFiles()[0]
return ""

def shutdown(self):
log.info("Application shutting down.")
# Clear QML context property to prevent TypeErrors during shutdown
if self.engine:
log.info("Clearing uiState context property in QML.")
self.engine.rootContext().setContextProperty("uiState", None)
del self.engine # Explicitly delete the engine

self.watcher.stop()
Expand All @@ -280,16 +353,24 @@ def _get_stack_info(self, index: int) -> str:
log.info(f"_get_stack_info for index {index}: {info}")
return info

def main(image_dir: Path = typer.Argument(..., help="Directory of images to view")):
def main(image_dir: Optional[Path] = typer.Argument(None, help="Directory of images to view")):
"""FastStack Application Entry Point"""
setup_logging()
log.info("Starting FastStack")

if image_dir is None:
image_dir_str = config.get('core', 'default_directory')
if not image_dir_str:
log.error("No image directory provided and no default directory set in the settings.")
# In a real app, we might open a dialog here to ask for a directory.
sys.exit(1)
image_dir = Path(image_dir_str)
Comment thread
AlanRockefeller marked this conversation as resolved.

if not image_dir.is_dir():
log.error(f"Image directory not found: {image_dir}")
sys.exit(1)

app = QGuiApplication(sys.argv)
app = QApplication(sys.argv)
app.setOrganizationName("FastStack")
app.setOrganizationDomain("faststack.dev")
app.setApplicationName("FastStack")
Expand Down
4 changes: 3 additions & 1 deletion faststack/faststack/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@

DEFAULT_CONFIG = {
"core": {
"cache_bytes": str(int(1.5 * 1024**3)), # 1.5 GB
"cache_size_gb": "1.5",
"prefetch_radius": "4",
"theme": "dark",
"default_directory": "",
},
"helicon": {
"exe": "C:\\Program Files\\Helicon Software\\Helicon Focus 8\\HeliconFocus.exe",
Expand Down
2 changes: 2 additions & 0 deletions faststack/faststack/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class EntryMetadata:
flag: bool = False
reject: bool = False
stack_id: Optional[int] = None
stacked: bool = False
stacked_date: Optional[str] = None


@dataclasses.dataclass
Expand Down
3 changes: 1 addition & 2 deletions faststack/faststack/qml/Components.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ Item {
// The main image display
Image {
id: mainImage
width: parent.width
height: parent.height
anchors.fill: parent
source: uiState && uiState.imageCount > 0 ? uiState.currentImageSource : ""
fillMode: Image.PreserveAspectFit
cache: false // We do our own caching in Python
Expand Down
Loading