Aggiunge i18n IT/EN/FR/DE/ES, pagine pubbliche squadre e UX archivio.
Il selettore lingua funziona sul web e sulle app native; su Android la preferenza è persistita e applicata al riavvio. Incluse anche eliminazione replay a scope e campi pagina pubblica squadra. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module LocaleResolver
|
||||
COOKIE_NAME = "mltv_locale"
|
||||
COOKIE_MAX_AGE = 2.years.to_i
|
||||
|
||||
module_function
|
||||
|
||||
def available
|
||||
I18n.available_locales.map(&:to_sym)
|
||||
end
|
||||
|
||||
def normalize(value)
|
||||
code = value.to_s.strip.downcase.split(/[-_]/).first
|
||||
return nil if code.blank?
|
||||
|
||||
sym = code.to_sym
|
||||
available.include?(sym) ? sym : nil
|
||||
end
|
||||
|
||||
def from_cookie(cookies)
|
||||
return nil if cookies.nil?
|
||||
|
||||
value = if cookies.respond_to?(:[])
|
||||
cookies[COOKIE_NAME]
|
||||
elsif cookies.respond_to?(:fetch)
|
||||
cookies.fetch(COOKIE_NAME, nil)
|
||||
end
|
||||
normalize(value)
|
||||
end
|
||||
|
||||
def from_accept_language(header)
|
||||
return nil if header.blank?
|
||||
|
||||
header.to_s.split(",").each do |part|
|
||||
tag = part.split(";").first.to_s.strip
|
||||
locale = normalize(tag)
|
||||
return locale if locale
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def resolve(cookie_jar: nil, cookies: nil, accept_language: nil)
|
||||
jar = cookie_jar || cookies
|
||||
from_cookie(jar) || from_accept_language(accept_language) || I18n.default_locale
|
||||
end
|
||||
|
||||
def persist!(cookie_jar, locale)
|
||||
normalized = normalize(locale) || I18n.default_locale
|
||||
cookie_jar[COOKIE_NAME] = {
|
||||
value: normalized.to_s,
|
||||
expires: COOKIE_MAX_AGE.seconds.from_now,
|
||||
path: "/",
|
||||
same_site: :lax,
|
||||
httponly: false
|
||||
}
|
||||
normalized
|
||||
end
|
||||
|
||||
def language_options
|
||||
[
|
||||
{ code: :it, label: "Italiano", native: "Italiano", flag: "🇮🇹" },
|
||||
{ code: :en, label: "English", native: "English", flag: "🇬🇧" },
|
||||
{ code: :fr, label: "Français", native: "Français", flag: "🇫🇷" },
|
||||
{ code: :de, label: "Deutsch", native: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: :es, label: "Español", native: "Español", flag: "🇪🇸" }
|
||||
].select { |opt| available.include?(opt[:code]) }
|
||||
end
|
||||
|
||||
def current_language_option
|
||||
language_options.find { |opt| opt[:code] == I18n.locale.to_sym } || language_options.first
|
||||
end
|
||||
end
|
||||
@@ -1,11 +1,33 @@
|
||||
module Recordings
|
||||
class Delete
|
||||
def initialize(recording, reason: :manual)
|
||||
SCOPES = %w[both site youtube].freeze
|
||||
|
||||
def initialize(recording, reason: :manual, scope: :both)
|
||||
@recording = recording
|
||||
@reason = reason
|
||||
@scope = normalize_scope(scope)
|
||||
end
|
||||
|
||||
def call
|
||||
case @scope
|
||||
when "youtube"
|
||||
delete_youtube_only!
|
||||
when "site"
|
||||
delete_site_only!
|
||||
else
|
||||
delete_both!
|
||||
end
|
||||
@recording
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_scope(scope)
|
||||
value = scope.to_s.presence || "both"
|
||||
SCOPES.include?(value) ? value : "both"
|
||||
end
|
||||
|
||||
def delete_both!
|
||||
delete_youtube_video
|
||||
delete_storage_object if @recording.storage_key.present?
|
||||
cleanup_local_artifacts
|
||||
@@ -18,10 +40,27 @@ module Recordings
|
||||
youtube_video_id: nil,
|
||||
youtube_published_at: nil
|
||||
)
|
||||
@recording
|
||||
end
|
||||
|
||||
private
|
||||
def delete_site_only!
|
||||
delete_storage_object if @recording.storage_key.present?
|
||||
cleanup_local_artifacts
|
||||
|
||||
@recording.update!(
|
||||
status: "expired",
|
||||
deleted_at: Time.current,
|
||||
storage_key: nil,
|
||||
thumbnail_storage_key: nil
|
||||
)
|
||||
end
|
||||
|
||||
def delete_youtube_only!
|
||||
delete_youtube_video
|
||||
@recording.update!(
|
||||
youtube_video_id: nil,
|
||||
youtube_published_at: nil
|
||||
)
|
||||
end
|
||||
|
||||
def delete_youtube_video
|
||||
return if @recording.youtube_video_id.blank?
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
module Teams
|
||||
class GenerateSlug
|
||||
def self.call(team)
|
||||
new(team).call
|
||||
end
|
||||
|
||||
def initialize(team)
|
||||
@team = team
|
||||
end
|
||||
|
||||
def call
|
||||
base = @team.name.to_s.parameterize.presence || "squadra"
|
||||
slug = base
|
||||
n = 2
|
||||
while conflict?(slug)
|
||||
slug = "#{base}-#{n}"
|
||||
n += 1
|
||||
end
|
||||
slug
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def conflict?(slug)
|
||||
scope = Team.where(slug: slug)
|
||||
scope = scope.where.not(id: @team.id) if @team.persisted?
|
||||
scope.exists?
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,144 @@
|
||||
module Teams
|
||||
# Elenco pubblico delle squadre con presenza su Match Live TV (diretta, replay o calendario).
|
||||
class PublicDirectory
|
||||
Entry = Struct.new(
|
||||
:team,
|
||||
:live_now,
|
||||
:replay_count,
|
||||
:upcoming_count,
|
||||
:last_activity_at,
|
||||
keyword_init: true
|
||||
)
|
||||
|
||||
def self.call(q: nil, sport: nil, live: false, replays: false)
|
||||
new(q: q, sport: sport, live: live, replays: replays).call
|
||||
end
|
||||
|
||||
def initialize(q: nil, sport: nil, live: false, replays: false)
|
||||
@q = q.to_s.strip
|
||||
@sport = normalize_sport(sport)
|
||||
@live = cast_bool(live)
|
||||
@replays = cast_bool(replays)
|
||||
end
|
||||
|
||||
def call
|
||||
teams = base_teams
|
||||
return [] if teams.empty?
|
||||
|
||||
entries = build_entries(teams)
|
||||
entries = apply_toggle_filters(entries)
|
||||
sort_entries(entries)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_sport(value)
|
||||
key = value.to_s.strip
|
||||
return nil if key.blank?
|
||||
return nil unless Sports::Catalog.find_optional(key)
|
||||
|
||||
Sports::Catalog.normalize_key(key)
|
||||
end
|
||||
|
||||
def cast_bool(value)
|
||||
ActiveModel::Type::Boolean.new.cast(value)
|
||||
end
|
||||
|
||||
def base_teams
|
||||
scope = Team.includes(:club).where(id: active_team_ids)
|
||||
scope = scope.where(sport_key: @sport) if @sport
|
||||
scope = apply_search(scope) if @q.present?
|
||||
scope.to_a
|
||||
end
|
||||
|
||||
def active_team_ids
|
||||
live = live_team_ids
|
||||
replays = replay_team_ids
|
||||
upcoming = upcoming_team_ids
|
||||
(live + replays + upcoming).uniq
|
||||
end
|
||||
|
||||
def live_team_ids
|
||||
StreamSession.broadcasting
|
||||
.publicly_listed
|
||||
.where(platform: "matchlivetv")
|
||||
.joins(:match)
|
||||
.distinct
|
||||
.pluck("matches.team_id")
|
||||
end
|
||||
|
||||
def replay_team_ids
|
||||
Recording.ready.publicly_listed.joins(stream_session: :match).distinct.pluck("matches.team_id")
|
||||
end
|
||||
|
||||
def upcoming_team_ids
|
||||
broadcasting_match_ids = StreamSession.broadcasting.select(:match_id)
|
||||
Match.scheduled_for_live
|
||||
.where.not(id: broadcasting_match_ids)
|
||||
.distinct
|
||||
.pluck(:team_id)
|
||||
end
|
||||
|
||||
def apply_search(scope)
|
||||
term = "%#{ActiveRecord::Base.sanitize_sql_like(@q)}%"
|
||||
scope.left_joins(:club, :matches).where(
|
||||
"teams.name ILIKE :term OR clubs.name ILIKE :term OR matches.location ILIKE :term OR matches.opponent_name ILIKE :term",
|
||||
term: term
|
||||
).distinct
|
||||
end
|
||||
|
||||
def build_entries(teams)
|
||||
team_ids = teams.map(&:id)
|
||||
live_set = live_team_ids.to_set
|
||||
replay_counts = Recording.ready.publicly_listed
|
||||
.joins(stream_session: :match)
|
||||
.where(matches: { team_id: team_ids })
|
||||
.group("matches.team_id")
|
||||
.count
|
||||
last_replay_at = Recording.ready.publicly_listed
|
||||
.joins(stream_session: :match)
|
||||
.where(matches: { team_id: team_ids })
|
||||
.group("matches.team_id")
|
||||
.maximum(:recorded_at)
|
||||
last_live_at = StreamSession.broadcasting
|
||||
.publicly_listed
|
||||
.joins(:match)
|
||||
.where(matches: { team_id: team_ids })
|
||||
.group("matches.team_id")
|
||||
.maximum(:started_at)
|
||||
broadcasting_match_ids = StreamSession.broadcasting.select(:match_id)
|
||||
upcoming_counts = Match.scheduled_for_live
|
||||
.where(team_id: team_ids)
|
||||
.where.not(id: broadcasting_match_ids)
|
||||
.group(:team_id)
|
||||
.count
|
||||
|
||||
teams.map do |team|
|
||||
last_activity = [last_replay_at[team.id], last_live_at[team.id]].compact.max
|
||||
Entry.new(
|
||||
team: team,
|
||||
live_now: live_set.include?(team.id),
|
||||
replay_count: replay_counts[team.id].to_i,
|
||||
upcoming_count: upcoming_counts[team.id].to_i,
|
||||
last_activity_at: last_activity
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def apply_toggle_filters(entries)
|
||||
entries = entries.select(&:live_now) if @live
|
||||
entries = entries.select { |entry| entry.replay_count.positive? } if @replays
|
||||
entries
|
||||
end
|
||||
|
||||
def sort_entries(entries)
|
||||
entries.sort_by do |entry|
|
||||
[
|
||||
entry.live_now ? 0 : 1,
|
||||
-(entry.last_activity_at&.to_i || 0),
|
||||
entry.team.name.downcase
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user