diff --git a/lintunes/__init__.py b/lintunes/__init__.py
index 6ae27d0..0097ab3 100644
--- a/lintunes/__init__.py
+++ b/lintunes/__init__.py
@@ -1,3 +1,3 @@
"""LinTunes — iTunes-style music library manager and player for Linux."""
-__version__ = "0.49.0"
+__version__ = "0.49.1"
diff --git a/lintunes/gui/export_dialog.py b/lintunes/gui/export_dialog.py
index dbe8f2c..563d15b 100644
--- a/lintunes/gui/export_dialog.py
+++ b/lintunes/gui/export_dialog.py
@@ -19,8 +19,7 @@ from PyQt6.QtWidgets import (
from lintunes import theme
from lintunes.export import exporter, upload
-
-_IMAGE_FILTER = "Images (*.png *.jpg *.jpeg *.gif *.webp)"
+from lintunes.gui.file_filters import WEB_IMAGE_FILTER as _IMAGE_FILTER
class ExportKindDialog(QDialog):
diff --git a/lintunes/gui/file_filters.py b/lintunes/gui/file_filters.py
new file mode 100644
index 0000000..c00d4b3
--- /dev/null
+++ b/lintunes/gui/file_filters.py
@@ -0,0 +1,17 @@
+"""File-dialog name filters.
+
+Qt matches a filter's patterns case-sensitively on Linux, so "*.jpg" hides
+a phone photo called IMG_1897.JPG. Every pattern is listed in both cases.
+"""
+
+_IMAGE_SUFFIXES = ("png", "jpg", "jpeg", "gif", "bmp", "webp")
+
+
+def image_filter(suffixes=_IMAGE_SUFFIXES) -> str:
+ patterns = " ".join(f"*.{s} *.{s.upper()}" for s in suffixes)
+ return f"Images ({patterns})"
+
+
+IMAGE_FILTER = image_filter()
+# What a web page can show: no BMP.
+WEB_IMAGE_FILTER = image_filter(("png", "jpg", "jpeg", "gif", "webp"))
diff --git a/lintunes/gui/info_dialog.py b/lintunes/gui/info_dialog.py
index e511540..dd64940 100644
--- a/lintunes/gui/info_dialog.py
+++ b/lintunes/gui/info_dialog.py
@@ -13,6 +13,7 @@ from PyQt6.QtGui import QPixmap, QImage, QKeySequence, QShortcut
from lintunes import tagging
from lintunes.gui.art_ops import embed_artwork
from lintunes.player import VOLUME_ADJUSTMENT_MAX
+from lintunes.gui.file_filters import IMAGE_FILTER
log = logging.getLogger(__name__)
@@ -206,7 +207,7 @@ class ArtSquare(QLabel):
def choose_file(self):
file_name, _filter = QFileDialog.getOpenFileName(
self, "Choose Artwork", "",
- "Images (*.jpg *.jpeg *.png *.gif *.bmp *.webp)")
+ IMAGE_FILTER)
if file_name and not self._stage_file(Path(file_name)):
self.hint_changed.emit("That file isn't a readable image")
diff --git a/lintunes/gui/sync_settings_dialog.py b/lintunes/gui/sync_settings_dialog.py
index 722a519..6907e14 100644
--- a/lintunes/gui/sync_settings_dialog.py
+++ b/lintunes/gui/sync_settings_dialog.py
@@ -24,6 +24,7 @@ from lintunes.cassette import syncthing_api as st
from lintunes.cassette.state import (
FOLLOW_LIBRARY, FOLLOW_SEPARATE, LAYOUT_FOLDER, LAYOUT_MIXED, OPEN, in_dir,
)
+from lintunes.gui.file_filters import IMAGE_FILTER
AVATAR_PX = 256
AVATAR_PREVIEW_PX = 72
@@ -244,7 +245,7 @@ class GeneralPage(QWidget):
def _choose_avatar(self):
path, _ = QFileDialog.getOpenFileName(
self, "Choose an Avatar", str(Path.home()),
- "Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)")
+ IMAGE_FILTER)
if not path:
return
image = load_avatar_image(Path(path))
diff --git a/tests/test_round81.py b/tests/test_round81.py
index d1ed72e..f5bdb39 100644
--- a/tests/test_round81.py
+++ b/tests/test_round81.py
@@ -535,3 +535,13 @@ def test_several_images_land_in_order_with_a_gap(tmp_path):
assert page[spots[0]:spots[1]].count("
") == 1
assert (dest / "cover 2.png").read_bytes() == b"B"
assert not (dest / "placeholder.png").exists()
+
+
+def test_image_filters_see_uppercase_suffixes():
+ """Qt filters are case-sensitive on Linux; a phone's IMG_1897.JPG hid."""
+ import fnmatch
+ from lintunes.gui.file_filters import IMAGE_FILTER, WEB_IMAGE_FILTER
+ for flt in (IMAGE_FILTER, WEB_IMAGE_FILTER):
+ patterns = flt[flt.index("(") + 1:-1].split()
+ for name in ("IMG_1897.JPG", "a.jpeg", "b.PNG", "c.WebP"[:-4] + "WEBP"):
+ assert any(fnmatch.fnmatchcase(name, p) for p in patterns), name