Skip to content
Open
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
27 changes: 23 additions & 4 deletions files/usr/share/cinnamon/cinnamon-settings/modules/cs_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,10 +212,29 @@ def _on_face_browse_menuitem_activated(self, menuitem):

face_path = os.path.join(self.accountService.get_home_dir(), ".face")

pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 255, -1)
pixbuf.savev(face_path, "png")
self.accountService.set_icon_file(path)
self.face_button.set_picture_from_file(path)
# Resize image if needed to avoid AccountsService size limits
# Load original to check size
original_pixbuf = GdkPixbuf.Pixbuf.new_from_file(path)
width = original_pixbuf.get_width()
height = original_pixbuf.get_height()

# Resize if larger than 512px (AccountsService limit is ~1MB, 512px is safe)
max_size = 512
if width > max_size or height > max_size:
# Calculate aspect-preserving dimensions
scale = min(max_size / width, max_size / height)
new_width = int(width * scale)
new_height = int(height * scale)
pixbuf = original_pixbuf.scale_simple(new_width, new_height, GdkPixbuf.InterpType.BILINEAR)
else:
pixbuf = original_pixbuf

# Save resized/original image to .face
pixbuf.savev(face_path, "png", [], [])

# Use .face path (not original) for AccountsService
self.accountService.set_icon_file(face_path)
self.face_button.set_picture_from_file(face_path)

dialog.destroy()

Expand Down
93 changes: 92 additions & 1 deletion js/ui/userWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

const Atk = imports.gi.Atk;
const Clutter = imports.gi.Clutter;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Gio = imports.gi.Gio;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const St = imports.gi.St;
Expand All @@ -13,6 +15,13 @@ const Params = imports.misc.params;

var AVATAR_ICON_SIZE = 64;

// Directory for cached avatar copies (to bypass St's image caching)
// Using ~/.cache for persistence across reboots
const AVATAR_CACHE_DIR = GLib.build_filenamev([GLib.get_user_cache_dir(), 'cinnamon', 'avatars']);

// Maximum dimensions for avatar images (AccountsService has size limits)
const MAX_AVATAR_SIZE = 512;

var Avatar = GObject.registerClass(
class Avatar extends St.Bin {
_init(user, params) {
Expand Down Expand Up @@ -106,6 +115,86 @@ class Avatar extends St.Bin {
this.update();
}

// Get a cache-busted path for the icon file
// This copies (and optionally resizes) the file to ~/.cache to bypass St's image cache
_getCacheBustedIconPath(iconFile) {
// Only needed for AccountsService icons (same path, changing content)
if (!iconFile.startsWith('/var/lib/AccountsService/icons/'))
return iconFile;

try {
// Ensure cache directory exists
let cacheDir = Gio.File.new_for_path(AVATAR_CACHE_DIR);
if (!cacheDir.query_exists(null)) {
cacheDir.make_directory_with_parents(null);
}

// Get file modification time for cache-busting
let file = Gio.File.new_for_path(iconFile);
let info = file.query_info('time::modified', Gio.FileQueryInfoFlags.NONE, null);
let mtime = info.get_modification_date_time().to_unix();

// Get username from path
let username = GLib.path_get_basename(iconFile);

// Create unique filename with timestamp
let cachedPath = GLib.build_filenamev([AVATAR_CACHE_DIR, `${username}-${mtime}`]);

// Check if cached file already exists
let cachedFile = Gio.File.new_for_path(cachedPath);
if (!cachedFile.query_exists(null)) {
// Clean up old cached files for this user
this._cleanOldCachedAvatars(username, cachedPath);

// Load image with GdkPixbuf to check dimensions
let pixbuf = GdkPixbuf.Pixbuf.new_from_file(iconFile);
let width = pixbuf.get_width();
let height = pixbuf.get_height();

// If image is larger than MAX_AVATAR_SIZE, resize it
if (width > MAX_AVATAR_SIZE || height > MAX_AVATAR_SIZE) {
// Calculate aspect-preserving dimensions
let scale = Math.min(MAX_AVATAR_SIZE / width, MAX_AVATAR_SIZE / height);
let newWidth = Math.floor(width * scale);
let newHeight = Math.floor(height * scale);

// Resize and save
let scaledPixbuf = pixbuf.scale_simple(newWidth, newHeight, GdkPixbuf.InterpType.BILINEAR);
scaledPixbuf.savev(cachedPath, 'png', [], []);
} else {
// Image is small enough, just copy it
file.copy(cachedFile, Gio.FileCopyFlags.OVERWRITE, null, null);
}
}

return cachedPath;
} catch (e) {
global.logError(`[UserWidget] Failed to create cached avatar: ${e}`);
return iconFile;
}
}

// Remove old cached avatars for a user (keep only the current one)
_cleanOldCachedAvatars(username, keepPath) {
try {
let cacheDir = Gio.File.new_for_path(AVATAR_CACHE_DIR);
let enumerator = cacheDir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
let info;
while ((info = enumerator.next_file(null)) !== null) {
let name = info.get_name();
if (name.startsWith(username + '-')) {
let filePath = GLib.build_filenamev([AVATAR_CACHE_DIR, name]);
if (filePath !== keepPath) {
let file = Gio.File.new_for_path(filePath);
file.delete(null);
}
}
}
} catch (e) {
// Ignore cleanup errors
}
}

update() {
let iconFile = null;
if (this._user) {
Expand All @@ -120,10 +209,12 @@ class Avatar extends St.Bin {
this._iconSize * scaleFactor);

if (iconFile) {
// Use cache-busted path to bypass St's image caching
let displayPath = this._getCacheBustedIconPath(iconFile);
this.child = null;
this.add_style_class_name('user-avatar');
this.style = `
background-image: url("${iconFile}");
background-image: url("${displayPath}");
background-size: cover;`;
} else {
this.style = null;
Expand Down