Compare commits

..

16 Commits

Author SHA1 Message Date
d69a193e6d refactor: consolidate Docker build process
Some checks failed
CI / scan_ruby (push) Waiting to run
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
Docker / docker (push) Has been cancelled
- Removed unused Dockerfile.base and Dockerfile.build.
- Combined build steps into the main Dockerfile for improved clarity and maintenance.
- Updated base image references accordingly.

This change streamlines the Docker build process by reducing the number of Dockerfiles and improving the clarity of dependency management.
2025-02-02 00:44:36 +08:00
2c72e96977 feat: switch to using official ruby slim image
Update the Dockerfile to use the official ruby slim image, rather than a custom image. This reduces the Docker image size and improves build times.

- Update the base build and development images to use the official ruby slim image
- Remove redundant arguments in the build and development stages

Using the official ruby slim image simplifies the build process and reduces the image size, resulting in faster build times and a more maintainable Dockerfile.
2025-02-02 00:40:59 +08:00
378530cc1b feat: automate base image build for GitHub Actions
- Added new GitHub Actions to docker-main.yml to build and push base images
- Created a new Dockerfile.base with the base packages installation
- Modified the existing Dockerfile to use the new base image
- Created a new Dockerfile.build with the build stage to reduce the size of the final image
2025-02-02 00:38:11 +08:00
536b97a7da feat: update latest weather art display
- Change latest weather art limit from 6 to 20
- Shuffle the collection and display the last 10 items
- Update the section title to reflect the shuffling

This commit enhances the visibility of the latest weather art by
increasing the limit of displayed items. It randomizes the selection
of the latest art pieces for a more dynamic user experience, while the
UI title is updated to clarify this feature. This change improves user
engagement with the content.
2025-02-02 00:21:51 +08:00
f2951e2741 feat: optimize batch weather art generation
- Introduce BATCH_SIZE constant to limit processed cities
- Shuffle and limit eligible cities processing to enhance worker efficiency

This update improves the performance of the BatchGenerateWeatherArtsWorker
by ensuring that only a set number of cities are processed within the
allotted time, reducing the risk of timeouts and making the overall
system more responsive.
2025-02-01 15:01:36 +08:00
9417358625 refactor: adjust image loading in weather art view
- Update image link to use original blob instead of a resized variant
- Remove the resizing option for the image tag for better fidelity
- Modify CSS class for a smoother hover effect without scaling issues

This refactor improves the image loading behavior by allowing
full-resolution images to be loaded directly. The previous resizing
was limiting image quality, and this change enhances user experience
when viewing weather art.
2025-02-01 14:58:51 +08:00
dependabot[bot]
31bd6fd74e build(deps): bump solid_cable from 3.0.5 to 3.0.7
Bumps [solid_cable](https://github.com/rails/solid_cable) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/rails/solid_cable/releases)
- [Commits](https://github.com/rails/solid_cable/compare/v3.0.5...v3.0.7)

---
updated-dependencies:
- dependency-name: solid_cable
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:47:06 +08:00
dependabot[bot]
e36448e6d7 chore(deps): bump solid_queue from 1.1.2 to 1.1.3
Bumps [solid_queue](https://github.com/rails/solid_queue) from 1.1.2 to 1.1.3.
- [Release notes](https://github.com/rails/solid_queue/releases)
- [Commits](https://github.com/rails/solid_queue/compare/v1.1.2...v1.1.3)

---
updated-dependencies:
- dependency-name: solid_queue
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:56 +08:00
dependabot[bot]
11df3a31d1 chore(deps): bump puma from 6.5.0 to 6.6.0
Bumps [puma](https://github.com/puma/puma) from 6.5.0 to 6.6.0.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v6.5.0...v6.6.0)

---
updated-dependencies:
- dependency-name: puma
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:42 +08:00
dependabot[bot]
2c3f8e1b8e chore(deps): bump aws-sdk-s3 from 1.177.0 to 1.179.0
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.177.0 to 1.179.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-01 14:46:33 +08:00
9a745dcee0 Merge branch 'dev' 2025-02-01 14:45:27 +08:00
09b3f06ad4 style: adjust pagination button sizes
- Change button classes from `btn` to `btn btn-xs` for all pagination buttons
- Ensure consistent styling across all pagination elements

These changes standardize the button sizes in the pagination component, improving the visual uniformity. The adjustment enhances user experience by making the pagination buttons more appropriately sized within the interface, without affecting functionality.
2025-02-01 14:45:17 +08:00
05449d1e7f chore: update PhotoSwipe import paths
- Change import for PhotoSwipeLightbox to 'photoswipe/lightbox'
- Change import for PhotoSwipe to 'photoswipe'

These updates reflect the new module structure of the PhotoSwipe library, ensuring that the controller utilizes the correct paths for improved functionality and maintainability.
2025-02-01 14:31:53 +08:00
905ec35fd8 feat: add photo swipe lightbox functionality
- Integrate PhotoSwipe library for enhanced image viewing
- Create PhotoSwipeLightBoxController to manage images
- Register lightbox controller in Stimulus framework
- Update views to include lightbox functionality
- Modify styles to accommodate new design elements

This commit introduces a new way for users to view images with
PhotoSwipe, improving the interactivity of the photo gallery. It
also includes adjustments to the layout and styles for better
presentation and user experience.
2025-02-01 14:19:19 +08:00
0352923c5b feat: update weather data report format
Some checks are pending
Docker / docker (push) Waiting to run
- Include actual update time in weather report
- Rename watermark worker file for clarity

This commit enhances the weather data report by
updating the format to include the actual update time
retrieved from the weather service API. Additionally,
the watermark worker file has been renamed to improve
readability and consistency in the naming convention.
2025-01-31 10:46:32 +08:00
bbf8dfc2e6 feat: add watermarking functionality to weather art
Some checks failed
Docker / docker (push) Has been cancelled
- Include the image_processing gem to handle image manipulation
- Create a background worker for adding watermarks to weather art images
- Update WeatherArt model to attach watermark images
- Add a new watermark image asset

This commit enhances the WeatherArt feature by allowing images to
have watermarks added asynchronously, improving the visual
presentation of the art. It ensures sufficient image dimensions
before processing and includes error handling for the worker.
2025-01-29 01:53:50 +08:00
21 changed files with 290 additions and 205 deletions

View File

@ -56,10 +56,12 @@ gem "ahoy_matey", "~> 5.2"
gem "ruby-openai", "~> 7.3"
gem "httparty", "~> 0.22.0"
gem "down", "~> 5.4"
gem "aws-sdk-s3", "~> 1.177"
gem "aws-sdk-s3", "~> 1.179"
gem "sidekiq", "~> 7.3"
gem "sidekiq-scheduler", "~> 5.0"
gem "image_processing", "~> 1.13"
group :development, :test do
# See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
gem "debug", platforms: %i[ mri windows ], require: "debug/prelude"

View File

@ -93,17 +93,17 @@ GEM
ruby2_keywords (>= 0.0.2)
ast (2.4.2)
aws-eventstream (1.3.0)
aws-partitions (1.1035.0)
aws-sdk-core (3.215.0)
aws-partitions (1.1043.0)
aws-sdk-core (3.217.0)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.96.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-kms (1.97.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.177.0)
aws-sdk-core (~> 3, >= 3.210.0)
aws-sdk-s3 (1.179.0)
aws-sdk-core (~> 3, >= 3.216.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
aws-sigv4 (1.11.0)
@ -164,6 +164,14 @@ GEM
multipart-post (~> 2.0)
faraday-net_http (3.4.0)
net-http (>= 0.5.0)
ffi (1.17.1-aarch64-linux-gnu)
ffi (1.17.1-aarch64-linux-musl)
ffi (1.17.1-arm-linux-gnu)
ffi (1.17.1-arm-linux-musl)
ffi (1.17.1-arm64-darwin)
ffi (1.17.1-x86_64-darwin)
ffi (1.17.1-x86_64-linux-gnu)
ffi (1.17.1-x86_64-linux-musl)
formtastic (5.0.0)
actionpack (>= 6.0.0)
formtastic_i18n (0.7.0)
@ -181,15 +189,19 @@ GEM
csv
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
i18n (1.14.6)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inherited_resources (1.14.0)
actionpack (>= 6.0)
has_scope (>= 0.6)
railties (>= 6.0)
responders (>= 2)
io-console (0.8.0)
irb (1.14.3)
irb (1.15.1)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
@ -240,6 +252,7 @@ GEM
matrix (0.4.2)
meta-tags (2.22.1)
actionpack (>= 6.0.0, < 8.1)
mini_magick (4.13.2)
mini_mime (1.1.5)
minitest (5.25.4)
msgpack (1.7.5)
@ -263,21 +276,21 @@ GEM
net-protocol
net-ssh (7.3.0)
nio4r (2.7.4)
nokogiri (1.18.1-aarch64-linux-gnu)
nokogiri (1.18.2-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-aarch64-linux-musl)
nokogiri (1.18.2-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.1-arm-linux-gnu)
nokogiri (1.18.2-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-arm-linux-musl)
nokogiri (1.18.2-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.1-arm64-darwin)
nokogiri (1.18.2-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-darwin)
nokogiri (1.18.2-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-gnu)
nokogiri (1.18.2-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.1-x86_64-linux-musl)
nokogiri (1.18.2-x86_64-linux-musl)
racc (~> 1.4)
orm_adapter (0.5.0)
ostruct (0.6.1)
@ -286,6 +299,9 @@ GEM
ast (~> 2.4.1)
racc
pg (1.5.9)
pp (0.6.2)
prettyprint
prettyprint (0.2.0)
propshaft (1.1.0)
actionpack (>= 7.0.0)
activesupport (>= 7.0.0)
@ -295,7 +311,7 @@ GEM
date
stringio
public_suffix (6.0.1)
puma (6.5.0)
puma (6.6.0)
nio4r (~> 2.0)
raabro (1.4.0)
racc (1.8.1)
@ -386,6 +402,9 @@ GEM
faraday (>= 1)
faraday-multipart (>= 1)
ruby-progressbar (1.13.0)
ruby-vips (2.2.2)
ffi (~> 1.12)
logger
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
rufus-scheduler (3.9.2)
@ -410,7 +429,7 @@ GEM
tilt (>= 1.4.0, < 3)
sitemap_generator (6.3.0)
builder (~> 3.0)
solid_cable (3.0.5)
solid_cable (3.0.7)
actioncable (>= 7.2)
activejob (>= 7.2)
activerecord (>= 7.2)
@ -419,7 +438,7 @@ GEM
activejob (>= 7.2)
activerecord (>= 7.2)
railties (>= 7.2)
solid_queue (1.1.2)
solid_queue (1.1.3)
activejob (>= 7.1)
activerecord (>= 7.1)
concurrent-ruby (>= 1.3.1)
@ -492,7 +511,7 @@ PLATFORMS
DEPENDENCIES
activeadmin (~> 3.2)
ahoy_matey (~> 5.2)
aws-sdk-s3 (~> 1.177)
aws-sdk-s3 (~> 1.179)
bootsnap
brakeman
capybara
@ -502,6 +521,7 @@ DEPENDENCIES
down (~> 5.4)
friendly_id (~> 5.5)
httparty (~> 0.22.0)
image_processing (~> 1.13)
jbuilder
jsbundling-rails
kamal

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -1,3 +1,4 @@
@import "photoswipe/dist/photoswipe.css";
@tailwind base;
@tailwind components;

View File

@ -1,6 +1,6 @@
class HomeController < ApplicationController
def index
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(6)
@latest_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(20).shuffle.last(10)
@featured_arts = WeatherArt.includes(:city).order(created_at: :desc).limit(5)
set_meta_tags(
title: "AI-Generated Weather Art",

View File

@ -1,5 +1,6 @@
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "@hotwired/stimulus"
import "@fontsource/playfair-display/400.css";
import "@fontsource/playfair-display/700.css";
import "@fontsource/raleway/400.css";

View File

@ -6,3 +6,9 @@ import { application } from "./application"
import HelloController from "./hello_controller"
application.register("hello", HelloController)
import PhotoSwipeLightBoxController from "./photo_swipe_lightbox_controller"
console.log("ready to register photo-swipe")
application.register("photo-swipe-lightbox", PhotoSwipeLightBoxController)
console.log("successful to register photo-swipe")

View File

@ -0,0 +1,42 @@
import { Controller } from "@hotwired/stimulus"
import PhotoSwipeLightbox from 'photoswipe/lightbox'
import PhotoSwipe from 'photoswipe'
import 'photoswipe/dist/photoswipe.css'
export default class extends Controller {
static targets = ['image', 'gallery']
connect() {
this.initPhotoSwipeLightbox()
}
initPhotoSwipeLightbox() {
const lightbox = new PhotoSwipeLightbox({
gallery: this.galleryTarget,
children: 'a',
pswpModule: PhotoSwipe,
initialZoomInEndEvent: 'mousedown',
dataSource: (items) => {
return items.map((item) => ({
src: item.dataset.pswpSrc,
w: parseInt(item.dataset.pswpWidth, 10),
h: parseInt(item.dataset.pswpHeight, 10),
title: item.dataset.pswpCaption,
}))
},
padding: { top: 0, bottom: 0, left: 0, right: 0 }, // 自定义图片与页面边界的填充
closeOnScroll: false,
zoom: true, // 启用缩放功能
bgOpacity: 0.9, // 背景透明度
pswpUIOptions: {
arrowPrev: true,
arrowNext: true,
zoom: true, // 添加缩放按钮
fullscreen: true, // 添加全屏按钮
counter: true, // 显示当前图片编号
}
})
lightbox.init()
// console.log('PhotoSwipeLightbox instance:', lightbox);
}
}

View File

@ -4,6 +4,7 @@ class WeatherArt < ApplicationRecord
belongs_to :city
has_one_attached :image
has_one_attached :image_with_watermark
has_many :visits, class_name: "Ahoy::Visit", foreign_key: :weather_art_id
has_many :events, class_name: "Ahoy::Event", foreign_key: :weather_art_id
@ -50,4 +51,8 @@ class WeatherArt < ApplicationRecord
Ahoy::Event.where("properties::jsonb->>'event_type' = 'weather_art_view' AND (properties::jsonb->>'weather_art_id')::integer = ?", self.id).count
end
end
def image_url
image.attached? ? image.blob : nil
end
end

View File

@ -50,7 +50,7 @@ class AiService
- Temperature: #{weather_data[:temperature]}°C
- Weather: #{weather_data[:description]}
- Cloud cover: #{weather_data[:cloud]}%
- Time: Early morning
- Time: #{weather_data[:time]}
Requirements:
- Feature iconic landmarks or architecture from #{city.name}

View File

@ -31,7 +31,8 @@ class WeatherService
pressure: data["pressure"].to_f,
visibility: data["vis"].to_f,
cloud: data["cloud"].to_f,
description: data["text"]
description: data["text"],
time: response["updateTime"]
}
end
end

View File

@ -25,7 +25,7 @@
<!-- 最新天气艺术 -->
<section class="container mx-auto px-4 py-16 space-y-12">
<h2 class="text-3xl font-display font-bold text-center">Latest Weather Art</h2>
<h2 class="text-3xl font-display font-bold text-center">Shuffle Latest Weather Art</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
<% @latest_arts.each do |art| %>

View File

@ -33,22 +33,22 @@
<%# Includes all stylesheet files in app/assets/stylesheets %>
<%= javascript_include_tag "application", "data-turbo-track": "reload", type: "module" %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>
<!-- <script defer data-domain="todayaiweather.com" src="https://plausible.frytea.com/js/script.js"></script>-->
<script defer src="https://busuanzi.frytea.com/js"></script>
<!-- <script defer src="https://busuanzi.frytea.com/js"></script>-->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-PX1C92V5L7"></script>-->
<!-- <script>-->
<!-- window.dataLayer = window.dataLayer || [];-->
<!-- function gtag(){dataLayer.push(arguments);}-->
<!-- gtag('js', new Date());-->
gtag('config', 'G-PX1C92V5L7');
</script>
<!-- gtag('config', 'G-PX1C92V5L7');-->
<!-- </script>-->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"
crossorigin="anonymous"></script>
<!-- <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-7296634171837358"-->
<!-- crossorigin="anonymous"></script>-->
</head>

View File

@ -12,7 +12,7 @@
<div class="join shadow-lg">
<!-- 首页 -->
<%= link_to url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 19l-7-7 7-7m8 14l-7-7 7-7" />
</svg>
@ -20,7 +20,7 @@
<!-- 上一页 -->
<%= link_to url_for(page: collection.prev_page || 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
class: "join-item btn btn-xs #{collection.first_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
@ -33,35 +33,35 @@
<% if start_page > 1 %>
<%= link_to 1, url_for(page: 1, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% if start_page > 2 %>
<button class="join-item btn btn-ghost btn-disabled">...</button>
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %>
<% end %>
<% (start_page..end_page).each do |page| %>
<% if page == collection.current_page %>
<button class="join-item btn btn-ghost bg-primary/10 font-medium">
<button class="join-item btn btn-xs btn-ghost bg-primary/10 font-medium">
<%= page %>
</button>
<% else %>
<%= link_to page, url_for(page: page, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %>
<% end %>
<% if end_page < collection.total_pages %>
<% if end_page < collection.total_pages - 1 %>
<button class="join-item btn btn-ghost btn-disabled">...</button>
<button class="join-item btn btn-xs btn-ghost btn-disabled">...</button>
<% end %>
<%= link_to collection.total_pages,
url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn btn-ghost hover:bg-primary/5" %>
class: "join-item btn btn-xs btn-ghost hover:bg-primary/5" %>
<% end %>
<!-- 下一页 -->
<%= link_to url_for(page: collection.next_page || collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
@ -69,7 +69,7 @@
<!-- 末页 -->
<%= link_to url_for(page: collection.total_pages, region: params[:region], country: params[:country], sort: params[:sort]),
class: "join-item btn #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
class: "join-item btn btn-xs #{collection.last_page? ? 'btn-disabled' : 'btn-ghost'}" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>

View File

@ -1,11 +0,0 @@
<!-- app/views/weather_arts/_weather_stat.html.erb -->
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="<%= icon %>" />
</svg>
<div class="stat-title font-medium"><%= title %></div>
</div>
<div class="stat-value text-2xl"><%= value %></div>
<div class="stat-desc mt-1"><%= desc %></div>
</div>

View File

@ -0,0 +1,37 @@
<%# Partial _weather_stats.html.erb %>
<div class="stat bg-gradient-to-br from-primary/10 to-primary/20 hover:from-primary hover:to-primary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Temperature</div>
<div class="stat-value text-3xl"><%= weather_art.temperature %>°C</div>
<div class="stat-desc">Feels like <%= weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-gradient-to-br from-secondary/10 to-secondary/20 hover:from-secondary hover:to-secondary/30 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Wind</div>
<div class="stat-value text-3xl"><%= weather_art.wind_scale %></div>
<div class="stat-desc"><%= weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Humidity</div>
<div class="stat-value text-3xl"><%= weather_art.humidity %>%</div>
<div class="stat-desc">Relative humidity</div>
</div>
<div class="stat bg-base-300 hover:bg-base-400 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Visibility</div>
<div class="stat-value text-3xl"><%= weather_art.visibility %> km</div>
<div class="stat-desc">Clear view distance</div>
</div>
<div class="stat bg-accent/10 hover:bg-accent p-4 rounded-lg">
<div class="stat-title font-medium text-base">Pressure</div>
<div class="stat-value text-3xl"><%= weather_art.pressure %> hPa</div>
<div class="stat-desc">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200 hover:bg-base-100 p-4 rounded-lg">
<div class="stat-title font-medium text-base">Cloud Cover</div>
<div class="stat-value text-3xl"><%= weather_art.cloud %>%</div>
<div class="stat-desc">Sky coverage</div>
</div>

View File

@ -4,155 +4,88 @@
</script>
<% end %>
<div class="relative min-h-screen bg-base-200">
<!-- 背景图片 -->
<% if @weather_art.image.attached? %>
<div class="fixed inset-0 -z-10">
<%= image_tag @weather_art.image,
class: "absolute w-full h-full object-cover scale-110 filter blur-2xl opacity-25" %>
<div class="absolute inset-0 bg-gradient-to-b from-base-200/90 to-base-200/70 backdrop-blur-md"></div>
</div>
<% end %>
<div class="relative min-h-screen bg-white"> <!-- 使用更明快的背景颜色 -->
<div class="container mx-auto px-4 pt-12 pb-16">
<div class="max-w-6xl mx-auto space-y-6">
<!-- 主要内容 -->
<div class="relative z-10">
<!-- 返回导航 -->
<div class="container mx-auto px-4 py-6">
<%= link_to city_path(@weather_art.city),
class: "btn btn-ghost btn-lg gap-2 bg-base-100/50 backdrop-blur-sm hover:bg-base-100/70 transition-all duration-300" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to <%= @weather_art.city.name %>
<% end %>
</div>
<!-- 返回导航 -->
<div class="flex items-center">
<%= link_to city_path(@weather_art.city),
class: "btn btn-ghost btn-md gap-2 bg-base-200 hover:bg-base-300 transition-all duration-300" do %>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
Back to <%= @weather_art.city.name %>
<% end %>
</div>
<div class="container mx-auto px-4 pb-16">
<div class="max-w-6xl mx-auto">
<!-- 头部信息 -->
<div class="text-center space-y-4 mb-12">
<div class="inline-flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-full bg-base-100/50 backdrop-blur-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
<%= @weather_art.city.full_name %>
</div>
<!-- 主要内容 -->
<div class="card bg-base-200/80 backdrop-blur-md shadow-lg overflow-hidden"> <!-- 调整透明度和阴影 -->
<div class="grid lg:grid-cols-2 gap-6 items-center">
<h1 class="text-4xl md:text-6xl font-display font-bold">
<span class="bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
Weather Art
</span>
</h1>
<!-- 图片区域 -->
<% if @weather_art.image.attached? %>
<figure class="relative lg:h-[30rem] h-auto overflow-hidden rounded-lg"> <!-- 添加圆角 -->
<div class="gallery" data-controller="photo-swipe-lightbox">
<div data-photo-swipe-lightbox-target="gallery" class="h-full">
<% blob = @weather_art.image_blob %>
<%= link_to rails_blob_path(blob),
data: {
pswp_src: rails_blob_url(blob),
pswp_caption: 'Weather Art',
pswp_width: 1792,
pswp_height: 1024
} do %>
<%= image_tag @weather_art.image,
class: "object-cover w-full h-full transition-transform transform hover:scale-105 ease-in-out" %> <!-- 改变缩放效果 -->
<% end %>
</div>
</div>
</figure>
<% end %>
<div class="flex flex-wrap justify-center items-center gap-3">
<div class="badge badge-lg badge-primary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
</div>
<div class="badge badge-lg badge-secondary gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<%= @weather_art.weather_date.strftime("%H:%M") %>
</div>
</div>
</div>
<!-- 信息区域 -->
<div class="card-body p-8 lg:py-10 lg:px-12">
<div class="prose max-w-none">
<h1 class="font-display text-4xl md:text-5xl font-bold text-gradient mb-6">
Weather Art
</h1>
<!-- 主要卡片 -->
<div class="card lg:card-side bg-base-100/80 backdrop-blur-md shadow-2xl">
<figure class="lg:w-1/2 relative aspect-square lg:aspect-auto group">
<% if @weather_art.image.attached? %>
<%= image_tag @weather_art.image,
class: "w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" %>
<div class="absolute inset-0 bg-gradient-to-t from-base-100/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<% end %>
</figure>
<div class="flex flex-wrap gap-4 mb-6">
<div class="badge badge-lg badge-primary">
<%= @weather_art.weather_date.strftime("%B %d, %Y") %>
</div>
<div class="badge badge-lg badge-secondary">
<%= @weather_art.weather_date.strftime("%H:%M") %>
</div>
</div>
<div class="card-body lg:w-1/2">
<div class="prose max-w-none mb-8">
<h2 class="card-title font-display text-3xl mb-4 flex items-center gap-3">
<h2 class="text-2xl font-semibold mb-4">
<%= weather_description_icon(@weather_art.description) %>
<%= @weather_art.description %>
</h2>
<div class="divider"></div>
</div>
<!-- 天气数据网格 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("temperature") %>
<div class="stat-title font-medium">Temperature</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.temperature %>°C</div>
<div class="stat-desc mt-1">Feels like <%= @weather_art.feeling_temp %>°C</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("wind") %>
<div class="stat-title font-medium">Wind</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.wind_scale %></div>
<div class="stat-desc mt-1"><%= @weather_art.wind_speed %> km/h</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("humidity") %>
<div class="stat-title font-medium">Humidity</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.humidity %>%</div>
<div class="stat-desc mt-1">Relative humidity</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("visibility") %>
<div class="stat-title font-medium">Visibility</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.visibility %> km</div>
<div class="stat-desc mt-1">Clear view distance</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("pressure") %>
<div class="stat-title font-medium">Pressure</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.pressure %> hPa</div>
<div class="stat-desc mt-1">Atmospheric pressure</div>
</div>
<div class="stat bg-base-200/50 backdrop-blur-sm rounded-box hover:bg-base-300/50 transition-all duration-300">
<div class="flex items-center gap-2 mb-2">
<%= weather_stat_icon("cloud") %>
<div class="stat-title font-medium">Cloud Cover</div>
</div>
<div class="stat-value text-2xl"><%= @weather_art.cloud %>%</div>
<div class="stat-desc mt-1">Sky coverage</div>
</div>
</div>
<!-- AI Prompt -->
<div class="mt-8">
<div class="bg-base-200/50 backdrop-blur-sm p-6 rounded-box border border-base-300">
<div class="flex items-center gap-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
</div>
<p class="text-base-content/70 leading-relaxed">
<%= @weather_art.prompt %>
</p>
<div class="grid grid-cols-2 gap-4">
<%= render 'weather_stats', weather_art: @weather_art %> <!-- 使用局部渲染 -->
</div>
</div>
</div>
</div>
</div>
<!-- AI Prompt -->
<div class="bg-primary/10 backdrop-blur-md p-6 rounded-lg border border-primary/20">
<div class="flex items-center gap-3 mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<h3 class="font-display font-bold text-lg">AI Prompt</h3>
</div>
<p class="text-base-content/80 leading-relaxed">
<%= @weather_art.prompt %>
</p>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,53 @@
class AddWatermarkToWeatherArtWorker
include Sidekiq::Worker
def perform(weather_art_id)
@weather_art = WeatherArt.find_by(id: weather_art_id)
return unless @weather_art
add_watermark
rescue StandardError => e
Rails.logger.error "Error adding watermark to WeatherArt #{weather_art_id}: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
end
private
attr_reader :weather_art
def add_watermark
return if weather_art.image_with_watermark.attached?
watermark_path = Rails.root.join("app/assets/images/today_ai_weather_copyright_watermark1.png")
return unless File.exist?(watermark_path)
image_tempfile = nil
watermark_tempfile = nil
begin
image_tempfile = weather_art.image.download
return unless image_dimensions_are_sufficient?(image_tempfile.path)
image = ImageProcessing::Vips.source(image_tempfile.path)
watermark = ImageProcessing::Vips.source(watermark_path)
watermarked_image = image.composite(watermark, "overlay")
watermark_tempfile = Tempfile.new([ "watermarked_image", ".png" ])
watermarked_image.write_to_file(watermark_tempfile.path)
weather_art.image_with_watermark.attach(
io: File.open(watermark_tempfile.path),
filename: "#{generate_filename("watermarked")}",
content_type: "image/png"
)
ensure
watermark_tempfile.unlink if watermark_tempfile
end
end
def image_dimensions_are_sufficient?(image_path)
dimensions = ImageProcessing::Vips.source(image_path).sizes
dimensions.width >= 200 && dimensions.height >= 200
end
def generate_filename(prefix)
"#{prefix}_#{weather_art.image.filename.base}"
end
end

View File

@ -1,19 +1,16 @@
class BatchGenerateWeatherArtsWorker
include Sidekiq::Worker
GENERATION_INTERVAL = 24.hours
MAX_DURATION = 50.minutes
SLEEP_DURATION = 120.seconds
BATCH_SIZE = 20
def perform(*args)
start_time = Time.current
cities_to_process = get_eligible_cities
cities_to_process = get_eligible_cities.shuffle.take(BATCH_SIZE)
cities_to_process.each do |city|
break if Time.current - start_time > MAX_DURATION
Rails.logger.info "Generating weather art for #{city.name}"
GenerateWeatherArtWorker.perform_async(city.id)
sleep SLEEP_DURATION
end
@ -23,7 +20,6 @@ class BatchGenerateWeatherArtsWorker
def get_eligible_cities
cutoff_time = Time.current - GENERATION_INTERVAL
City.active
.joins("LEFT JOIN (
SELECT city_id, MAX(created_at) as last_generation_time

View File

@ -18,6 +18,7 @@
"autoprefixer": "^10.4.20",
"jquery": "^3.7.1",
"jquery-ui": "^1.14.1",
"photoswipe": "^5.4.4",
"postcss": "^8.5.1",
"sass": "^1.83.4",
"tailwindcss": "^3.4.17"

View File

@ -153,7 +153,7 @@
"@hotwired/stimulus@^3.2.2":
version "3.2.2"
resolved "https://registry.npmjs.org/@hotwired/stimulus/-/stimulus-3.2.2.tgz"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.2.tgz#071aab59c600fed95b97939e605ff261a4251608"
integrity sha512-eGeIqNOQpXoPAIP7tC1+1Yc1yl1xnwYqg+3mzqxyrbE5pg5YFBZcA6YoTiByJB6DKAEsiWtl6tjTJS4IYtbB7A==
"@hotwired/turbo-rails@^8.0.12":
@ -722,20 +722,13 @@ jiti@^1.21.6:
resolved "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz"
integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
jquery-ui@^1.13.3:
jquery-ui@^1.13.3, jquery-ui@^1.14.1:
version "1.14.1"
resolved "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.14.1.tgz"
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
dependencies:
jquery ">=1.12.0 <5.0.0"
jquery-ui@^1.14.1:
version "1.14.1"
resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.14.1.tgz#ba342ea3ffff662b787595391f607d923313e040"
integrity sha512-DhzsYH8VeIvOaxwi+B/2BCsFFT5EGjShdzOcm5DssWjtcpGWIMsn66rJciDA6jBruzNiLf1q0KvwMoX1uGNvnQ==
dependencies:
jquery ">=1.12.0 <5.0.0"
jquery-ujs@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/jquery-ujs/-/jquery-ujs-1.2.3.tgz"
@ -853,6 +846,11 @@ path-scurry@^1.11.1:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
photoswipe@^5.4.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/photoswipe/-/photoswipe-5.4.4.tgz#e045dc036453493188d5c8665b0e8f1000ac4d6e"
integrity sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==
picocolors@^1, picocolors@^1.0.1, picocolors@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"