panda-core 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d1c4f83377c0b031d5a68ef550668e6ce9d0e09f9d0865da9ab7a5b7349d981
4
- data.tar.gz: c82ab418857a1b1961270f417f69ae9e236e1bb36fb3612b7c6e7001ac440cc9
3
+ metadata.gz: a2cf8c2a2e3d0ee0034e6d066e6b0091a91ac26b45175137bd20e55b428d0b27
4
+ data.tar.gz: dfce6c83389d46e65195b10ac115d6fde5ae27eb7aeaab791e102b12aa428917
5
5
  SHA512:
6
- metadata.gz: 3ab5424862f54e04222d4a8909487e0179e5a73ba1852cce491bd0767cfd26d177d02e720317c610d610275bd895f757cec61d337708a211fd12240db6d3fe47
7
- data.tar.gz: d24d95c56a5ceac68c0713e51cb029a63b27f88476bf1d93007ae6822a472ea456d919e833c0f26817489dee98af8407a3d6f958b8f2d73846a8b5a064e51dd1
6
+ metadata.gz: 733a874eeff0435314e2fde0ec445a35b13c3f5623bb4bb711d6bc6a5709bdc9bf07699cfa7c241868f3157c0421c3993b91a1f43c7704378aae0f7e07c5f74e
7
+ data.tar.gz: 3fed89c4393a1b33ab120308a40657189719c1a9b956afb822823eefda663989a259f769535fb00a587dd9e3136d70e6c0fcb4b8ceb0dbac4cc4b8511b001ff1
@@ -31,14 +31,15 @@ module Panda
31
31
 
32
32
  def render_avatar
33
33
  has_image = resolved_user.respond_to?(:avatar_url) &&
34
- resolved_user.avatar_url.present?
34
+ resolved_user.avatar_url(size: :small).present?
35
35
 
36
36
  if has_image
37
37
  div do
38
38
  img(
39
39
  class: "inline-block w-10 h-10 rounded-full object-cover",
40
- src: resolved_user.avatar_url,
41
- alt: ""
40
+ src: resolved_user.avatar_url(size: :small),
41
+ alt: "",
42
+ loading: "lazy"
42
43
  )
43
44
  end
44
45
  else
@@ -5,8 +5,13 @@ module Panda
5
5
  class User < ApplicationRecord
6
6
  self.table_name = "panda_core_users"
7
7
 
8
- # Active Storage attachment for avatar
9
- has_one_attached :avatar
8
+ # Active Storage attachment for avatar with variants
9
+ has_one_attached :avatar do |attachable|
10
+ attachable.variant :thumb, resize_to_limit: [50, 50], preprocessed: true
11
+ attachable.variant :small, resize_to_limit: [100, 100], preprocessed: true
12
+ attachable.variant :medium, resize_to_limit: [200, 200], preprocessed: true
13
+ attachable.variant :large, resize_to_limit: [400, 400], preprocessed: true
14
+ end
10
15
 
11
16
  validates :email, presence: true, uniqueness: {case_sensitive: false}
12
17
 
@@ -90,9 +95,15 @@ module Panda
90
95
 
91
96
  # Returns the URL for the user's avatar
92
97
  # Prefers Active Storage attachment over OAuth provider URL
93
- def avatar_url
98
+ # @param size [Symbol] The variant size (:thumb, :small, :medium, :large, or nil for original)
99
+ # @return [String, nil] The avatar URL or nil if no avatar available
100
+ def avatar_url(size: nil)
94
101
  if avatar.attached?
95
- Rails.application.routes.url_helpers.rails_blob_path(avatar, only_path: true)
102
+ if size && [:thumb, :small, :medium, :large].include?(size)
103
+ Rails.application.routes.url_helpers.rails_blob_path(avatar.variant(size), only_path: true)
104
+ else
105
+ Rails.application.routes.url_helpers.rails_blob_path(avatar, only_path: true)
106
+ end
96
107
  elsif self[:image_url].present?
97
108
  # Fallback to OAuth provider URL if no avatar is attached yet
98
109
  self[:image_url]
@@ -5,6 +5,9 @@ require "open-uri"
5
5
  module Panda
6
6
  module Core
7
7
  class AttachAvatarService < Services::BaseService
8
+ MAX_FILE_SIZE = 5.megabytes
9
+ MAX_DIMENSION = 800 # Max width/height for optimization
10
+
8
11
  def initialize(user:, avatar_url:)
9
12
  @user = user
10
13
  @avatar_url = avatar_url
@@ -33,25 +36,67 @@ module Panda
33
36
  URI.open(@avatar_url, read_timeout: 10, open_timeout: 10, redirect: true) do |downloaded_file|
34
37
  # standard:enable Security/Open
35
38
  # Validate file size (max 5MB)
36
- if downloaded_file.size > 5.megabytes
39
+ if downloaded_file.size > MAX_FILE_SIZE
37
40
  raise "Avatar file too large (#{downloaded_file.size} bytes)"
38
41
  end
39
42
 
40
43
  # Determine content type and filename
41
44
  content_type = downloaded_file.content_type || "image/jpeg"
42
- extension = determine_extension(content_type)
43
- filename = "avatar_#{@user.id}_#{Time.current.to_i}#{extension}"
44
45
 
45
- # Attach the avatar
46
+ # Optimize and attach the avatar
47
+ optimized_file = optimize_image(downloaded_file, content_type)
48
+
49
+ filename = "avatar_#{@user.id}_#{Time.current.to_i}.webp"
50
+
51
+ # Attach the optimized avatar
46
52
  @user.avatar.attach(
47
- io: downloaded_file,
53
+ io: optimized_file,
48
54
  filename: filename,
49
- content_type: content_type
55
+ content_type: "image/webp"
50
56
  )
51
57
  end
52
58
  # standard:enable Security/Open
53
59
  end
54
60
 
61
+ def optimize_image(file, content_type)
62
+ processor = Panda::Core.config.avatar_image_processor
63
+ loader = load_image_processor(processor)
64
+ return file unless loader
65
+
66
+ begin
67
+ processed = loader
68
+ .source(file)
69
+ .resize_to_limit(MAX_DIMENSION, MAX_DIMENSION)
70
+ .convert("webp")
71
+ .saver(quality: 85, strip: true) # Strip metadata, 85% quality
72
+ .call
73
+
74
+ # Return File object
75
+ File.open(processed.path)
76
+ rescue => e
77
+ Rails.logger.warn("Image optimization failed (#{processor}), using original: #{e.message}")
78
+ # Fallback to original file if optimization fails
79
+ file
80
+ end
81
+ end
82
+
83
+ def load_image_processor(processor)
84
+ case processor
85
+ when :vips
86
+ require "image_processing/vips"
87
+ ImageProcessing::Vips
88
+ when :mini_magick
89
+ require "image_processing/mini_magick"
90
+ ImageProcessing::MiniMagick
91
+ else
92
+ Rails.logger.warn("Unknown image processor: #{processor}, avatar optimization disabled")
93
+ nil
94
+ end
95
+ rescue LoadError => e
96
+ Rails.logger.warn("Image processor #{processor} not available: #{e.message}")
97
+ nil
98
+ end
99
+
55
100
  def determine_extension(content_type)
56
101
  case content_type
57
102
  when /jpeg|jpg/
@@ -24,7 +24,12 @@ module Panda
24
24
  :login_page_title,
25
25
  :admin_title,
26
26
  :initial_admin_breadcrumb,
27
- :dashboard_redirect_path
27
+ :dashboard_redirect_path,
28
+ :avatar_variants,
29
+ :avatar_max_file_size,
30
+ :avatar_max_dimension,
31
+ :avatar_optimization_quality,
32
+ :avatar_image_processor
28
33
 
29
34
  def initialize
30
35
  @user_class = "Panda::Core::User"
@@ -80,6 +85,18 @@ module Panda
80
85
  @admin_title = "Panda Admin"
81
86
  @initial_admin_breadcrumb = nil # Proc that returns [label, path]
82
87
  @dashboard_redirect_path = nil # Path to redirect to after login (defaults to admin_root_path)
88
+
89
+ # Avatar configuration
90
+ @avatar_variants = {
91
+ thumb: {resize_to_limit: [50, 50]},
92
+ small: {resize_to_limit: [100, 100]},
93
+ medium: {resize_to_limit: [200, 200]},
94
+ large: {resize_to_limit: [400, 400]}
95
+ }
96
+ @avatar_max_file_size = 5.megabytes
97
+ @avatar_max_dimension = 800
98
+ @avatar_optimization_quality = 85
99
+ @avatar_image_processor = :vips # or :mini_magick
83
100
  end
84
101
  end
85
102
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Panda
4
4
  module Core
5
- VERSION = "0.7.3"
5
+ VERSION = "0.7.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: panda-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Otaina Limited
@@ -10,6 +10,20 @@ bindir: bin
10
10
  cert_chain: []
11
11
  date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: image_processing
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: importmap-rails
15
29
  requirement: !ruby/object:Gem::Requirement