Skip to content

Commit 013f38b

Browse files
committed
Add support for screenshot on plugins
1 parent db43500 commit 013f38b

File tree

14 files changed

+873
-73
lines changed

14 files changed

+873
-73
lines changed

qgis-app/plugins/api.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def plugin_upload(package, **kwargs):
5050
try:
5151
# JSON-RPC cannot deserialize base64 strings to bytes, do it here instead
5252
if isinstance(package, str):
53-
package = b64decode(package.encode('utf-8'))
53+
package = b64decode(package.encode("utf-8"))
5454

5555
request = kwargs.get("request")
5656
package = BytesIO(package)
@@ -130,10 +130,17 @@ def plugin_upload(package, **kwargs):
130130
version_data["changelog"] = cleaned_data.get("changelog")
131131
if cleaned_data.get("qgisMaximumVersion"):
132132
version_data["max_qg_version"] = cleaned_data.get("qgisMaximumVersion")
133+
if cleaned_data.get("screenshot_file"):
134+
version_data["screenshot"] = cleaned_data.get("screenshot_file")
133135

134136
new_version = PluginVersion(**version_data)
135137
new_version.clean()
136138
new_version.save()
139+
140+
# Update plugin-level screenshot when version has one
141+
if cleaned_data.get("screenshot_file"):
142+
plugin.screenshot = cleaned_data.get("screenshot_file")
143+
plugin.save(update_fields=["screenshot"])
137144
except IntegrityError as e:
138145
# Avoids error: current transaction is aborted, commands ignored until
139146
# end of transaction block

qgis-app/plugins/forms.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class Meta:
5656
fields = (
5757
"description",
5858
"about",
59+
"screenshot",
5960
"author",
6061
"email",
6162
"icon",
@@ -191,6 +192,11 @@ def clean(self):
191192
self.instance.experimental = self.cleaned_data.get("experimental")
192193
if "supportsQt6" in self.cleaned_data:
193194
self.instance.supports_qt6 = self.cleaned_data.get("supportsQt6")
195+
196+
# Handle screenshot from package only
197+
if self.cleaned_data.get("screenshot_file"):
198+
self.instance.screenshot = self.cleaned_data.get("screenshot_file")
199+
194200
return super(PluginVersionForm, self).clean()
195201

196202

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.27 on 2026-02-04 05:07
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("plugins", "0018_pluginversionsecurityscan"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="pluginversion",
15+
name="screenshot",
16+
field=models.ImageField(
17+
blank=True,
18+
help_text="Optional preview screenshot for the plugin. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF.",
19+
null=True,
20+
upload_to="packages/%Y",
21+
verbose_name="Screenshot",
22+
),
23+
),
24+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.27 on 2026-02-04 05:44
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("plugins", "0019_pluginversion_screenshot"),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name="plugin",
15+
name="screenshot",
16+
field=models.ImageField(
17+
blank=True,
18+
help_text="Default preview screenshot for the plugin. This will be used if no version-specific screenshot is available. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF.",
19+
null=True,
20+
upload_to="packages/%Y",
21+
verbose_name="Screenshot",
22+
),
23+
),
24+
]

qgis-app/plugins/models.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,16 @@ class Plugin(models.Model):
538538
_("Icon"), blank=True, null=True, upload_to=PLUGINS_STORAGE_PATH
539539
)
540540

541+
screenshot = models.ImageField(
542+
_("Screenshot"),
543+
blank=True,
544+
null=True,
545+
upload_to=PLUGINS_STORAGE_PATH,
546+
help_text=_(
547+
"Default preview screenshot for the plugin. This will be used if no version-specific screenshot is available. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF."
548+
),
549+
)
550+
541551
# downloads (soft trigger from versions)
542552
downloads = models.IntegerField(_("Downloads"), default=0, editable=False)
543553

@@ -918,6 +928,16 @@ class PluginVersion(models.Model):
918928

919929
# the file!
920930
package = models.FileField(_("Plugin package"), upload_to=PLUGINS_STORAGE_PATH)
931+
# Preview screenshot
932+
screenshot = models.ImageField(
933+
_("Screenshot"),
934+
blank=True,
935+
null=True,
936+
upload_to=PLUGINS_STORAGE_PATH,
937+
help_text=_(
938+
"Optional preview screenshot for the plugin. Maximum size: 2MB. Supported formats: PNG, JPG, JPEG, GIF."
939+
),
940+
)
921941
# Flags: checks on unique current/experimental are done in save() and possibly in the views
922942
experimental = models.BooleanField(
923943
_("Experimental flag"),
@@ -1142,14 +1162,30 @@ def delete_version_package(sender, instance, **kw):
11421162
pass
11431163

11441164

1165+
def delete_version_screenshot(sender, instance, **kw):
1166+
"""
1167+
Removes the version screenshot
1168+
"""
1169+
try:
1170+
if instance.screenshot:
1171+
instance.screenshot.delete(False)
1172+
except:
1173+
pass
1174+
1175+
11451176
def delete_plugin_icon(sender, instance, **kw):
11461177
"""
1147-
Removes the plugin icon
1178+
Removes the plugin icon and screenshot
11481179
"""
11491180
try:
11501181
instance.icon.delete(False)
11511182
except:
11521183
pass
1184+
try:
1185+
if instance.screenshot:
1186+
instance.screenshot.delete(False)
1187+
except:
1188+
pass
11531189

11541190

11551191
def delete_feedback_attachment(sender, instance, **kw):
@@ -1241,6 +1277,7 @@ def pass_rate(self):
12411277

12421278

12431279
models.signals.post_delete.connect(delete_version_package, sender=PluginVersion)
1280+
models.signals.post_delete.connect(delete_version_screenshot, sender=PluginVersion)
12441281
models.signals.post_delete.connect(delete_plugin_icon, sender=Plugin)
12451282
models.signals.post_delete.connect(
12461283
delete_feedback_attachment, sender=PluginVersionFeedbackAttachment

qgis-app/plugins/templates/plugins/form_snippet.html

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
{% for error in field.errors %}
2121
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
2222
{% endfor %}
23-
{% endif %}
23+
{% endif %}
2424
</div>
2525
{% elif field.field.widget|klass == 'ClearableFileInput' %}
2626
<div class="file has-name">
@@ -37,35 +37,35 @@
3737
</span>
3838
<span class="file-label"> {{ field.label }} </span>
3939
</span>
40-
<span class="file-name" id="filename"> Choose a file… </span>
40+
<span class="file-name" id="filename-{{ field.html_name }}"> Choose a file… </span>
4141
</label>
4242
</div>
4343
{% if field.errors %}
4444
{% for error in field.errors %}
4545
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
4646
{% endfor %}
47-
{% endif %}
47+
{% endif %}
4848
{% elif field.field.widget|klass == 'Textarea' %}
4949
<div class="field">
5050
<label class="label">{{ field.label_tag }}</label>
5151
<div class="control">
52-
<textarea
53-
class="textarea"
54-
placeholder="{{ field.help_text | safe }}"
52+
<textarea
53+
class="textarea"
54+
placeholder="{{ field.help_text | safe }}"
5555
id="{{ field.html_name }}"
5656
name="{{ field.html_name }}"
5757
>{{ field.value|default_if_none:'' }}</textarea>
5858
</div>
5959
{% if field.errors %}
6060
<p class="help is-danger">{{ field.errors }}</p>
61-
{% endif %}
61+
{% endif %}
6262
</div>
6363
{% elif field.field.widget|klass == 'TextInput' or field.field.widget|klass == 'URLInput' %}
6464
<div class="field">
6565
<label class="label">{{ field.label_tag }}</label>
6666
<div class="control">
67-
<input class="input" type="text"
68-
placeholder="{{ field.help_text | safe }}"
67+
<input class="input" type="text"
68+
placeholder="{{ field.help_text | safe }}"
6969
id="{{ field.html_name }}"
7070
name="{{ field.html_name }}"
7171
value="{{ field.value|default_if_none:'' }}"
@@ -75,13 +75,13 @@
7575
{% for error in field.errors %}
7676
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
7777
{% endfor %}
78-
{% endif %}
78+
{% endif %}
7979
</div>
8080
{% elif field.field.widget|klass == 'EmailInput' %}
8181
<div class="field">
8282
<label class="label">{{ field.label_tag }}</label>
8383
<div class="control has-icons-left">
84-
<input class="input" type="email"
84+
<input class="input" type="email"
8585
id="{{ field.html_name }}"
8686
name="{{ field.html_name }}"
8787
value="{{ field.value|default_if_none:'' }}"
@@ -94,7 +94,7 @@
9494
{% for error in field.errors %}
9595
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
9696
{% endfor %}
97-
{% endif %}
97+
{% endif %}
9898
</div>
9999
{% elif field.field.widget|klass == 'Select' %}
100100
<div class="field">
@@ -114,9 +114,9 @@
114114
{% for error in field.errors %}
115115
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
116116
{% endfor %}
117-
{% endif %}
117+
{% endif %}
118118
</div>
119-
{% else %}
119+
{% else %}
120120
<div class="field">
121121
<label class="label">{{ field.label_tag }}</label>
122122
<div class="control">
@@ -126,7 +126,7 @@
126126
{% for error in field.errors %}
127127
<p class="help is-danger has-text-weight-medium">{{ error }}</p>
128128
{% endfor %}
129-
{% endif %}
129+
{% endif %}
130130
</div>
131131
{% endif %}
132132
<div class="help has-text-grey">{{ field.help_text | safe }}</div>
@@ -136,15 +136,21 @@
136136

137137
<script>
138138
document.addEventListener("DOMContentLoaded", function () {
139-
const fileInput = document.querySelector(".file-input");
140-
const fileName = document.querySelector("#filename");
139+
const fileInputs = document.querySelectorAll(".file-input");
140+
141+
fileInputs.forEach(function (fileInput) {
142+
fileInput.addEventListener("change", function () {
143+
const fieldName = fileInput.getAttribute("name");
144+
const fileName = document.querySelector("#filename-" + fieldName);
141145

142-
fileInput.addEventListener("change", function () {
143-
if (fileInput.files.length > 0) {
144-
fileName.textContent = fileInput.files[0].name;
145-
} else {
146-
fileName.textContent = "Choose a file…";
147-
}
146+
if (fileName) {
147+
if (fileInput.files.length > 0) {
148+
fileName.textContent = fileInput.files[0].name;
149+
} else {
150+
fileName.textContent = "Choose a file…";
151+
}
152+
}
153+
});
148154
});
149155
});
150156
</script>

qgis-app/plugins/templates/plugins/plugin_detail.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,15 @@ <h2>
407407
{% if object.about %}
408408
<div class="tab-pane is-active" id="plugin-about">
409409
<p>{{ object.about|linebreaksbr }}</p>
410+
411+
{% comment %}Display plugin screenshot if available{% endcomment %}
412+
{% if object.screenshot %}
413+
<div class="mt-4">
414+
<figure class="image">
415+
<img src="{{ object.screenshot.url }}" alt="{% trans "Plugin screenshot" %}" style="max-width: 100%; height: auto;">
416+
</figure>
417+
</div>
418+
{% endif %}
410419
</div>
411420
{% endif %}
412421
<div class="tab-pane{% if not object.about %} is-active{% endif %}" id="plugin-details">

qgis-app/plugins/templates/plugins/version_detail.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,16 @@ <h2 class="title is-4">{% trans "Version" %}: {{ version }}</h2>
157157
</div>
158158
</div>
159159
</div>
160+
161+
{% comment %}Display screenshot if available{% endcomment %}
162+
{% if version.screenshot %}
163+
<div class="mt-4">
164+
<label class="label has-text-weight-bold">{% trans "Preview Screenshot" %}</label>
165+
<figure class="image">
166+
<img src="{{ version.screenshot.url }}" alt="{% trans "Plugin screenshot" %}" style="max-width: 100%; height: auto;">
167+
</figure>
168+
</div>
169+
{% endif %}
160170
</div>
161171

162172
{% if user.is_staff or user in version.plugin.editors %}

0 commit comments

Comments
 (0)