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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a2cf8c2a2e3d0ee0034e6d066e6b0091a91ac26b45175137bd20e55b428d0b27
|
|
4
|
+
data.tar.gz: dfce6c83389d46e65195b10ac115d6fde5ae27eb7aeaab791e102b12aa428917
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
-
#
|
|
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:
|
|
53
|
+
io: optimized_file,
|
|
48
54
|
filename: filename,
|
|
49
|
-
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
|
|
data/lib/panda/core/version.rb
CHANGED
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.
|
|
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
|