Skip to content

Commit 01e6338

Browse files
picture-frames@KopfdesDaemons: Initial release (#1601)
1 parent c137d55 commit 01e6338

File tree

11 files changed

+620
-0
lines changed

11 files changed

+620
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Picture frames
2+
3+
## Image sources of the images in the screenshot
4+
5+
- [Picure 1](https://commons.wikimedia.org/wiki/File:Air_Happiness_%28Unsplash%29.jpg) [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.en)
6+
- [Picture 2](https://commons.wikimedia.org/wiki/File:Dog_Breeds.jpg) [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)
7+
- [Picture 3](https://commons.wikimedia.org/wiki/File:Tabby_cat_with_blue_eyes-3336579.jpg) [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/deed.en)
8+
- [Picture 4](<https://commons.wikimedia.org/wiki/File:Dromaius_novaehollandiae_(head)_Battersea_Park_Children%27s_Zoo.jpg>) [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/deed.en)
9+
- [Picture 5](https://commons.wikimedia.org/wiki/File:Crimea,_Ai-Petri,_low_clouds.jpg) [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/deed.en)
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
const Desklet = imports.ui.desklet;
2+
const St = imports.gi.St;
3+
const GLib = imports.gi.GLib;
4+
const Gettext = imports.gettext;
5+
const Settings = imports.ui.settings;
6+
const Cairo = imports.cairo;
7+
const GdkPixbuf = imports.gi.GdkPixbuf;
8+
const Gdk = imports.gi.Gdk;
9+
const Clutter = imports.gi.Clutter;
10+
const Cogl = imports.gi.Cogl;
11+
const Gio = imports.gi.Gio;
12+
13+
const UUID = "picture-frames@KopfdesDaemons";
14+
Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale");
15+
16+
function _(str) {
17+
return Gettext.dgettext(UUID, str);
18+
}
19+
20+
class MyDesklet extends Desklet.Desklet {
21+
constructor(metadata, deskletId) {
22+
super(metadata, deskletId);
23+
this.defaultImagePath = this.metadata.path + "/images/default.jpg";
24+
25+
// Setup settings and bind them to properties
26+
const settings = new Settings.DeskletSettings(this, metadata["uuid"], deskletId);
27+
settings.bindProperty(Settings.BindingDirection.IN, "image-path", "imagePath", this._initUI.bind(this));
28+
settings.bindProperty(Settings.BindingDirection.IN, "shape", "shape", this._initUI.bind(this));
29+
settings.bindProperty(Settings.BindingDirection.IN, "size", "size", this._initUI.bind(this));
30+
settings.bindProperty(Settings.BindingDirection.IN, "show-border", "showBorder", this._initUI.bind(this));
31+
settings.bindProperty(Settings.BindingDirection.IN, "border-color", "borderColor", this._initUI.bind(this));
32+
settings.bindProperty(Settings.BindingDirection.IN, "border-width", "borderWidth", this._initUI.bind(this));
33+
settings.bindProperty(Settings.BindingDirection.IN, "waves-number", "wavesNumber", this._initUI.bind(this));
34+
settings.bindProperty(Settings.BindingDirection.IN, "spikes-number", "spikesNumber", this._initUI.bind(this));
35+
settings.bindProperty(Settings.BindingDirection.IN, "wave-depth", "waveDepth", this._initUI.bind(this));
36+
settings.bindProperty(Settings.BindingDirection.IN, "spikes-depth", "spikesDepth", this._initUI.bind(this));
37+
settings.bindProperty(Settings.BindingDirection.IN, "align-x", "alignX", this._initUI.bind(this));
38+
settings.bindProperty(Settings.BindingDirection.IN, "align-y", "alignY", this._initUI.bind(this));
39+
40+
this.setHeader(_("Picture Frame"));
41+
this._initUI();
42+
}
43+
44+
_initUI() {
45+
const mainContainer = new St.BoxLayout({ vertical: true });
46+
if (!this.imagePath) this.imagePath = this.defaultImagePath;
47+
48+
const size = this.size;
49+
const finalImagePath = decodeURIComponent(this.imagePath.replace("file://", ""));
50+
const imageActor = this._createShapedImageActor(finalImagePath, size);
51+
52+
mainContainer.add_child(imageActor);
53+
this.setContent(mainContainer);
54+
}
55+
56+
_drawShapePath(cr, shape, centerX, centerY, radius) {
57+
switch (shape) {
58+
case "square":
59+
cr.rectangle(centerX - radius, centerY - radius, radius * 2, radius * 2);
60+
break;
61+
case "star":
62+
this._drawStarPath(cr, centerX, centerY, radius);
63+
break;
64+
case "wave":
65+
this._drawWavePath(cr, centerX, centerY, radius);
66+
break;
67+
case "heart":
68+
this._drawHeartPath(cr, centerX, centerY, radius);
69+
break;
70+
case "circle":
71+
default:
72+
cr.arc(centerX, centerY, radius, 0, 2 * Math.PI);
73+
cr.closePath();
74+
break;
75+
}
76+
}
77+
78+
_drawWavePath(cr, centerX, centerY, radius, numWaves = this.wavesNumber, amplitude = this.waveDepth / 100) {
79+
const baseRadius = radius * (1 - amplitude);
80+
const waveAmplitude = radius * amplitude;
81+
const points = 100; // Number of points for a smooth curve
82+
83+
// Move to the starting point
84+
const startAngle = 0;
85+
const startR = baseRadius + waveAmplitude * Math.sin(startAngle * numWaves);
86+
cr.moveTo(centerX + startR * Math.cos(startAngle), centerY + startR * Math.sin(startAngle));
87+
88+
for (let i = 1; i <= points; i++) {
89+
const angle = (i / points) * 2 * Math.PI;
90+
const r = baseRadius + waveAmplitude * Math.sin(angle * numWaves);
91+
cr.lineTo(centerX + r * Math.cos(angle), centerY + r * Math.sin(angle));
92+
}
93+
cr.closePath();
94+
}
95+
96+
_drawStarPath(cr, centerX, centerY, radius, numSpikes = this.spikesNumber) {
97+
const angleStep = (2 * Math.PI) / (numSpikes * 2);
98+
const outerRadius = radius;
99+
const innerRadius = (radius * (100 - this.spikesDepth)) / 100;
100+
101+
cr.moveTo(centerX + outerRadius, centerY);
102+
103+
for (let i = 1; i <= numSpikes * 2; i++) {
104+
const currentRadius = i % 2 === 1 ? innerRadius : outerRadius;
105+
const angle = i * angleStep;
106+
const x = centerX + currentRadius * Math.cos(angle);
107+
const y = centerY + currentRadius * Math.sin(angle);
108+
cr.lineTo(x, y);
109+
}
110+
cr.closePath();
111+
}
112+
113+
_drawHeartPath(cr, centerX, centerY, radius) {
114+
const yOffset = radius * 0.2; // Offset to center the heart vertically
115+
const topY = centerY - radius * 0.4 - yOffset;
116+
const bottomY = centerY + radius - yOffset;
117+
const rightX = centerX + radius;
118+
const leftX = centerX - radius;
119+
const rightCp1X = centerX + radius * 1.5;
120+
const leftCp1X = centerX - radius * 1.5;
121+
const cp2Y = centerY - radius - yOffset;
122+
123+
// Start at the bottom point
124+
cr.moveTo(centerX, bottomY);
125+
// Right side
126+
cr.curveTo(rightCp1X, centerY, rightX, cp2Y, centerX, topY);
127+
// Left side
128+
cr.curveTo(leftX, cp2Y, leftCp1X, centerY, centerX, bottomY);
129+
cr.closePath();
130+
}
131+
132+
_createShapedImageActor(imagePath, size) {
133+
const canvas = new Clutter.Canvas();
134+
canvas.set_size(size, size);
135+
const actor = new Clutter.Actor({ width: size, height: size, content: canvas });
136+
const file = Gio.file_new_for_path(imagePath);
137+
let pixbuf = null;
138+
139+
canvas.connect("draw", (canvas, cr, width, height) => {
140+
// Clear the canvas
141+
cr.save();
142+
cr.setOperator(Cairo.Operator.CLEAR);
143+
cr.paint();
144+
cr.restore();
145+
cr.setOperator(Cairo.Operator.OVER);
146+
147+
if (pixbuf === null) {
148+
// Draw loading text
149+
cr.setSourceRGBA(1.0, 1.0, 1.0, 0.7); // Semi-transparent white
150+
cr.selectFontFace("sans-serif", Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
151+
cr.setFontSize(20);
152+
const text = _("Loading...");
153+
const extents = cr.textExtents(text);
154+
cr.moveTo(width / 2 - extents.width / 2, height / 2);
155+
cr.showText(text);
156+
} else {
157+
// Draw the shaped image once pixbuf is loaded
158+
this._drawFinalImage(cr, pixbuf, width, height);
159+
}
160+
return true;
161+
});
162+
canvas.invalidate(); // Initial draw with "Loading..."
163+
164+
file.read_async(GLib.PRIORITY_DEFAULT, null, (source, res) => {
165+
try {
166+
const stream = source.read_finish(res);
167+
GdkPixbuf.Pixbuf.new_from_stream_async(stream, null, (source, res) => {
168+
try {
169+
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(res);
170+
canvas.invalidate(); // Force a redraw now that the pixbuf is loaded
171+
} catch (e) {
172+
global.logError(`Error creating pixbuf from stream: ${e}`);
173+
}
174+
});
175+
} catch (e) {
176+
global.logError(`Error reading file async: ${e}`);
177+
}
178+
});
179+
return actor;
180+
}
181+
182+
_drawFinalImage(cr, pixbuf, width, height) {
183+
try {
184+
// Preserve aspect ratio
185+
const originalWidth = pixbuf.get_width();
186+
const originalHeight = pixbuf.get_height();
187+
const aspect = originalWidth / originalHeight;
188+
189+
let newWidth, newHeight;
190+
if (aspect > 1) {
191+
// Wider than tall
192+
newHeight = height;
193+
newWidth = height * aspect;
194+
} else {
195+
// Taller than wide or square
196+
newWidth = width;
197+
newHeight = width / aspect;
198+
}
199+
200+
const scaledPixbuf = pixbuf.scale_simple(newWidth, newHeight, GdkPixbuf.InterpType.BILINEAR);
201+
const pixbufWithAlpha = scaledPixbuf.add_alpha(false, 0, 0, 0);
202+
203+
cr.save();
204+
this._drawShapePath(cr, this.shape, width / 2, height / 2, width / 2);
205+
cr.clip();
206+
207+
const drawX = (width - newWidth) * (this.alignX / 100);
208+
const drawY = (height - newHeight) * (this.alignY / 100);
209+
Gdk.cairo_set_source_pixbuf(cr, pixbufWithAlpha, drawX, drawY);
210+
cr.paint();
211+
cr.restore();
212+
213+
if (this.showBorder) {
214+
const borderWidth = this.borderWidth;
215+
const [success, color] = Clutter.Color.from_string(this.borderColor);
216+
217+
if (success) {
218+
cr.setSourceRGBA(color.red / 255, color.green / 255, color.blue / 255, color.alpha / 255);
219+
} else {
220+
// Fallback to white if color string is invalid
221+
cr.setSourceRGBA(1.0, 1.0, 1.0, 1.0);
222+
}
223+
cr.setLineWidth(borderWidth);
224+
this._drawShapePath(cr, this.shape, width / 2, height / 2, (width - borderWidth) / 2);
225+
cr.stroke();
226+
}
227+
} catch (e) {
228+
global.logError(`Error drawing shaped image: ${e}`);
229+
}
230+
}
231+
232+
// Helper to create an actor from a Pixbuf
233+
_createActorFromPixbuf(pixBuf) {
234+
const pixelFormat = pixBuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888;
235+
const image = new Clutter.Image();
236+
image.set_data(pixBuf.get_pixels(), pixelFormat, pixBuf.get_width(), pixBuf.get_height(), pixBuf.get_rowstride());
237+
238+
return new Clutter.Actor({
239+
content: image,
240+
width: pixBuf.get_width(),
241+
height: pixBuf.get_height(),
242+
});
243+
}
244+
245+
_getImageAtScale(imageFileName, requestedWidth, requestedHeight) {
246+
try {
247+
const pixBuf = GdkPixbuf.Pixbuf.new_from_file_at_size(imageFileName, requestedWidth, requestedHeight);
248+
return this._createActorFromPixbuf(pixBuf);
249+
} catch (e) {
250+
global.logError(`Error loading image ${imageFileName}: ${e}`);
251+
return new St.Label({ text: "Error" + e.message, style_class: "picture-frame-error-label" });
252+
}
253+
}
254+
}
255+
256+
function main(metadata, deskletId) {
257+
return new MyDesklet(metadata, deskletId);
258+
}
11.5 KB
Loading
127 KB
Loading
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"uuid": "picture-frames@KopfdesDaemons",
3+
"name": "Picture frames",
4+
"description": "Displays photos in different picture frames.",
5+
"version": "1.0",
6+
"prevent-decorations": true,
7+
"max-instances": "50"
8+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# PICTURE FRAMES
2+
# This file is put in the public domain.
3+
# KopfdesDaemons, 2025
4+
#
5+
#, fuzzy
6+
msgid ""
7+
msgstr ""
8+
"Project-Id-Version: picture-frames@KopfdesDaemons 1.0\n"
9+
"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-desklets/"
10+
"issues\n"
11+
"POT-Creation-Date: 2025-10-19 11:16+0200\n"
12+
"PO-Revision-Date: \n"
13+
"Last-Translator: \n"
14+
"Language-Team: \n"
15+
"Language: de\n"
16+
"MIME-Version: 1.0\n"
17+
"Content-Type: text/plain; charset=UTF-8\n"
18+
"Content-Transfer-Encoding: 8bit\n"
19+
"X-Generator: Poedit 3.4.2\n"
20+
21+
#. desklet.js:40
22+
msgid "Picture Frame"
23+
msgstr "Bilderrahmen"
24+
25+
#. desklet.js:152
26+
msgid "Loading..."
27+
msgstr "Lade..."
28+
29+
#. metadata.json->name
30+
msgid "Picture frames"
31+
msgstr "Bilderrahmen"
32+
33+
#. metadata.json->description
34+
msgid "Displays photos in different picture frames."
35+
msgstr "Zeigt Bilder in verschiedenen Bilderrahmen an."
36+
37+
#. settings-schema.json->head1->description
38+
msgid "Image Settings"
39+
msgstr "Bildeinstellungen"
40+
41+
#. settings-schema.json->image-path->description
42+
msgid "Image path"
43+
msgstr "Bildpfad"
44+
45+
#. settings-schema.json->image-path->tooltip
46+
msgid "The path of the image."
47+
msgstr "Der Pfad des Bildes."
48+
49+
#. settings-schema.json->size->description
50+
msgid "Size"
51+
msgstr "Größe"
52+
53+
#. settings-schema.json->align-x->description
54+
msgid "Horizontal alignment"
55+
msgstr "Horizontale Ausrichtung"
56+
57+
#. settings-schema.json->align-y->description
58+
msgid "Vertical alignment"
59+
msgstr "Vertikale Ausrichtung"
60+
61+
#. settings-schema.json->head2->description
62+
msgid "Frame Settings"
63+
msgstr "Rahmeneinstellungen"
64+
65+
#. settings-schema.json->shape->description
66+
msgid "Shape"
67+
msgstr "Form"
68+
69+
#. settings-schema.json->shape->options
70+
msgid "Circle"
71+
msgstr "Kreis"
72+
73+
#. settings-schema.json->shape->options
74+
msgid "Square"
75+
msgstr "Quadrat"
76+
77+
#. settings-schema.json->shape->options
78+
msgid "Star"
79+
msgstr "Stern"
80+
81+
#. settings-schema.json->shape->options
82+
msgid "Wave"
83+
msgstr "Welle"
84+
85+
#. settings-schema.json->shape->options
86+
msgid "Heart"
87+
msgstr "Herz"
88+
89+
#. settings-schema.json->waves-number->description
90+
msgid "Number of waves"
91+
msgstr "Anzahl der Wellen"
92+
93+
#. settings-schema.json->wave-depth->description
94+
msgid "Depth of the waves"
95+
msgstr "Tiefe der Wellen"
96+
97+
#. settings-schema.json->spikes-number->description
98+
msgid "Number of spikes"
99+
msgstr "Anzahl der Spitzen"
100+
101+
#. settings-schema.json->spikes-depth->description
102+
msgid "Depth of the spikes"
103+
msgstr "Tiefe der Spitzen"
104+
105+
#. settings-schema.json->show-border->description
106+
msgid "Show border"
107+
msgstr "Rand anzeigen"
108+
109+
#. settings-schema.json->border-color->description
110+
msgid "Border color"
111+
msgstr "Randfarbe"
112+
113+
#. settings-schema.json->border-width->description
114+
msgid "Border width"
115+
msgstr "Randbreite"

0 commit comments

Comments
 (0)