- Change description to use @weather_art.prompt instead of a translated string with multiple parameters. - Update image_alt method to include the prompt in the descriptive text. These changes simplify the metadata generation for weather art and improve the clarity of the image descriptions by incorporating the prompt, which may provide users with additional context.
267 lines
8.1 KiB
Ruby
267 lines
8.1 KiB
Ruby
class WeatherArt < ApplicationRecord
|
|
extend FriendlyId
|
|
friendly_id :weather_date, use: :slugged
|
|
|
|
belongs_to :city
|
|
has_one_attached :image
|
|
|
|
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
|
|
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
|
|
|
|
validates :weather_date, presence: true
|
|
validates :city_id, presence: true
|
|
|
|
scope :latest, ->(limit = 100) {
|
|
order(created_at: :desc).limit(limit)
|
|
}
|
|
|
|
scope :by_popularity, ->(limit = 100) {
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
|
joins("LEFT JOIN ahoy_events ON json_extract(ahoy_events.properties, '$.weather_art_id') = weather_arts.id
|
|
AND json_extract(ahoy_events.properties, '$.event_type') = 'weather_art_view'")
|
|
.group("weather_arts.id")
|
|
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
|
.order("visit_count DESC").limit(limit)
|
|
else
|
|
joins("LEFT JOIN ahoy_events ON (ahoy_events.properties::jsonb->>'weather_art_id')::integer = weather_arts.id
|
|
AND ahoy_events.properties::jsonb->>'event_type' = 'weather_art_view'")
|
|
.group("weather_arts.id")
|
|
.select("weather_arts.*, COUNT(ahoy_events.id) as visit_count")
|
|
.order("visit_count DESC").limit(limit)
|
|
end
|
|
}
|
|
|
|
scope :random, ->(limit = 3) {
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "postgresql"
|
|
# PostgreSQL 优化版本
|
|
order(Arel.sql("RANDOM()")).limit(limit)
|
|
elsif ActiveRecord::Base.connection.adapter_name.downcase == "mysql2"
|
|
# MySQL 优化版本
|
|
order(Arel.sql("RAND()")).limit(limit)
|
|
else
|
|
# SQLite 或其他数据库的通用版本
|
|
order(Arel.sql("RANDOM()")).limit(limit)
|
|
end
|
|
}
|
|
|
|
def should_generate_new_friendly_id?
|
|
weather_date_changed? || city_id_changed? || super
|
|
end
|
|
|
|
def to_s
|
|
"#{city.name} - #{weather_date.strftime('%Y-%m-%d')}"
|
|
end
|
|
|
|
def self.ransackable_associations(auth_object = nil)
|
|
[ "city", "image_attachment", "image_blob" ]
|
|
end
|
|
|
|
def self.ransackable_attributes(auth_object = nil)
|
|
[ "city_id", "cloud", "created_at", "description", "feeling_temp", "humidity", "id", "id_value", "precipitation", "pressure", "prompt", "temperature", "updated_at", "visibility", "weather_date", "wind_scale", "wind_speed" ]
|
|
end
|
|
|
|
def view_count
|
|
if ActiveRecord::Base.connection.adapter_name.downcase == "sqlite"
|
|
Ahoy::Event.where("json_extract(properties, '$.event_type') = 'weather_art_view' AND json_extract(properties, '$.weather_art_id') = ?", self.id).count
|
|
else
|
|
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
|
|
end
|
|
end
|
|
|
|
def formatted_time(type = :date, use_local_timezone = false)
|
|
# 获取时区
|
|
timezone_info = self.city&.country&.timezones.present? ?
|
|
JSON.parse(self.city.country.timezones)&.first :
|
|
{ "zoneName" => "UTC", "gmtOffsetName" => "UTC+00:00" }
|
|
|
|
# 设置时区对象
|
|
time_zone = ActiveSupport::TimeZone[timezone_info["zoneName"]] ||
|
|
ActiveSupport::TimeZone["UTC"]
|
|
time = self.updated_at
|
|
|
|
# 使用 I18n 本地化格式化日期
|
|
date_string = I18n.l(self.weather_date, format: :long)
|
|
|
|
# 格式化时间
|
|
time_format = use_local_timezone ? time.in_time_zone(time_zone) : time.utc
|
|
time_string =
|
|
if use_local_timezone
|
|
I18n.t("time.formats.with_zone",
|
|
time: I18n.l(time_format, format: :time_only),
|
|
zone: timezone_info["gmtOffsetName"]
|
|
)
|
|
else
|
|
I18n.t("time.formats.with_zone",
|
|
time: I18n.l(time_format, format: :time_only),
|
|
zone: "UTC"
|
|
)
|
|
end
|
|
|
|
case type
|
|
when :date
|
|
date_string
|
|
when :time
|
|
time_string
|
|
when :all
|
|
I18n.t("time.formats.date_and_time",
|
|
date: date_string,
|
|
time: time_string
|
|
)
|
|
else
|
|
I18n.t("time.formats.date_and_time",
|
|
date: date_string,
|
|
time: time_string
|
|
)
|
|
end
|
|
end
|
|
|
|
def image_url
|
|
image.attached? ? image.blob : nil
|
|
end
|
|
|
|
def webp_image
|
|
return nil unless image.attached?
|
|
|
|
image.variant(
|
|
format: "webp",
|
|
saver: {
|
|
quality: 100,
|
|
strip: true, # 移除元数据以减小文件大小
|
|
interlace: "plane" # 渐进式加载
|
|
}
|
|
)
|
|
end
|
|
|
|
# 添加图片变体处理
|
|
PREVIEW_DIMENSIONS = {
|
|
big: [ 1792, 1024 ],
|
|
medium: [ 896, 512 ],
|
|
small: [ 448, 256 ]
|
|
}.freeze
|
|
def preview_image(size = :big)
|
|
return nil unless image.attached?
|
|
|
|
width, height = PREVIEW_DIMENSIONS[size] || PREVIEW_DIMENSIONS[:medium]
|
|
|
|
image.variant(
|
|
resize_to_limit: [ width, height ],
|
|
format: "webp",
|
|
saver: {
|
|
quality: 75,
|
|
strip: true, # 移除元数据以减小文件大小
|
|
interlace: "plane" # 渐进式加载
|
|
}
|
|
)
|
|
end
|
|
|
|
def watermarked_image
|
|
return nil unless image.attached?
|
|
|
|
overlay_text = create_overlay_text
|
|
|
|
image.variant(
|
|
composite: [ {
|
|
input: overlay_text,
|
|
gravity: "southeast"
|
|
} ]
|
|
)
|
|
end
|
|
|
|
def image_alt
|
|
"#{self.city.full_name} Weather Art - #{self.description} at #{self.temperature}°C on #{self.formatted_time(:date)}, #{self.prompt}"
|
|
end
|
|
|
|
private
|
|
|
|
def create_overlay_text
|
|
{
|
|
create: {
|
|
width: 400,
|
|
height: 100,
|
|
background: [ 0, 0, 0, 0.5 ] # 半透明黑色背景
|
|
},
|
|
"svg-overlay": %(
|
|
<svg width="400" height="100">
|
|
<text x="20" y="40"
|
|
style="fill: white; font-family: Arial; font-size: 20px;">
|
|
#{city.name} - #{weather_date.strftime('%Y-%m-%d')}
|
|
</text>
|
|
<text x="20" y="70"
|
|
style="fill: white; font-family: Arial; font-size: 20px;">
|
|
© todayaiweather.com
|
|
</text>
|
|
</svg>
|
|
)
|
|
}
|
|
end
|
|
|
|
def create_text_layer(font_size)
|
|
text = [
|
|
weather_date.strftime("%Y-%m-%d"),
|
|
"#{temperature}°C, #{description}",
|
|
"#{city.name}, #{city.country.name}, #{city.country.region.name}",
|
|
"© todayaiweather.com"
|
|
].join("\n")
|
|
|
|
{
|
|
create: {
|
|
width: 600,
|
|
height: 200,
|
|
background: [ 0, 0, 0, 0 ]
|
|
},
|
|
"svg-overlay": %(
|
|
<svg width="600" height="200">
|
|
<style>
|
|
.text {
|
|
font-family: Arial, sans-serif;
|
|
font-size: #{font_size}px;
|
|
}
|
|
.shadow {
|
|
fill: white;
|
|
stroke: black;
|
|
stroke-width: 2px;
|
|
paint-order: stroke fill;
|
|
}
|
|
</style>
|
|
<text x="20" y="#{font_size + 10}" class="text shadow">#{weather_date.strftime('%Y-%m-%d')}</text>
|
|
<text x="20" y="#{font_size * 2 + 20}" class="text shadow">#{temperature}°C, #{description}</text>
|
|
<text x="20" y="#{font_size * 3 + 30}" class="text shadow">#{city.name}, #{city.country.name}</text>
|
|
<text x="20" y="#{font_size * 4 + 40}" class="text shadow">© todayaiweather.com</text>
|
|
</svg>
|
|
)
|
|
}
|
|
end
|
|
|
|
def watermark_command(font_size:, stroke_width:, spacing:)
|
|
date_str = weather_date.strftime("%Y-%m-%d")
|
|
weather_info = "#{temperature}°C, #{description}"
|
|
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
|
copyright = "© todayaiweather.com"
|
|
|
|
"gravity southeast " \
|
|
"fill white " \
|
|
"font Arial " \
|
|
"pointsize #{font_size} " \
|
|
"stroke black " \
|
|
"strokewidth #{stroke_width} " \
|
|
"text 30,#{spacing * 12} '#{copyright}' " \
|
|
"text 30,#{spacing * 8} '#{location_info}' " \
|
|
"text 30,#{spacing * 4} '#{weather_info}' " \
|
|
"text 30,#{spacing} '#{date_str}'"
|
|
end
|
|
|
|
def watermark_text
|
|
date_str = weather_date.strftime("%Y-%m-%d")
|
|
weather_info = "#{temperature}°C, #{description}"
|
|
location_info = "#{city.name}, #{city.country.name}, #{city.country.region.name}"
|
|
copyright = "© todayaiweather.com"
|
|
|
|
[
|
|
"text 30,120 '#{copyright}'",
|
|
"text 30,80 '#{location_info}'",
|
|
"text 30,40 '#{weather_info}'",
|
|
"text 30,0 '#{date_str}'"
|
|
].join(" ")
|
|
end
|
|
end
|