2026-03-27 21:45:40 -04:00
|
|
|
#include "ui.h"
|
|
|
|
|
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
#include <string.h>
|
|
|
|
|
|
|
|
|
|
static const char *FONT_CANDIDATES[] = {
|
|
|
|
|
"BigBlueTermPlus Nerd Font Mono.ttf",
|
|
|
|
|
"BigBlueTermPlusNerdFontMono-Regular.ttf",
|
|
|
|
|
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
|
|
|
|
"/usr/share/fonts/TTF/DejaVuSansMono.ttf",
|
|
|
|
|
"/usr/share/fonts/truetype/liberation2/LiberationMono-Regular.ttf"
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
|
2026-03-27 23:19:42 -04:00
|
|
|
static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height);
|
|
|
|
|
|
|
|
|
|
static void text_texture_destroy(UiTextTexture *text_texture) {
|
|
|
|
|
if (!text_texture) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (text_texture->texture) {
|
|
|
|
|
SDL_DestroyTexture(text_texture->texture);
|
|
|
|
|
}
|
|
|
|
|
memset(text_texture, 0, sizeof(*text_texture));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int text_texture_init(UiTextTexture *text_texture,
|
|
|
|
|
SDL_Renderer *renderer,
|
|
|
|
|
TTF_Font *font,
|
|
|
|
|
const char *text,
|
|
|
|
|
SDL_Color color) {
|
|
|
|
|
if (!text_texture) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(text_texture, 0, sizeof(*text_texture));
|
|
|
|
|
text_texture->texture = text_to_texture(renderer, font, text, color, &text_texture->width, &text_texture->height);
|
|
|
|
|
return text_texture->texture ? 0 : -1;
|
|
|
|
|
}
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
static void set_draw_color(SDL_Renderer *renderer, SDL_Color color) {
|
|
|
|
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height) {
|
|
|
|
|
SDL_Surface *surface;
|
|
|
|
|
SDL_Texture *texture;
|
|
|
|
|
|
|
|
|
|
if (!font || !text || text[0] == '\0') {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
surface = TTF_RenderUTF8_Blended(font, text, color);
|
|
|
|
|
if (!surface) {
|
|
|
|
|
return NULL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
texture = SDL_CreateTextureFromSurface(renderer, surface);
|
|
|
|
|
if (width) {
|
|
|
|
|
*width = surface->w;
|
|
|
|
|
}
|
|
|
|
|
if (height) {
|
|
|
|
|
*height = surface->h;
|
|
|
|
|
}
|
|
|
|
|
SDL_FreeSurface(surface);
|
|
|
|
|
return texture;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
static void draw_cached_text(SDL_Renderer *renderer, const UiTextTexture *text_texture, int x, int y) {
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_Rect dst;
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
if (!text_texture || !text_texture->texture) {
|
2026-03-27 21:45:40 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dst.x = x;
|
|
|
|
|
dst.y = y;
|
2026-03-27 23:19:42 -04:00
|
|
|
dst.w = text_texture->width;
|
|
|
|
|
dst.h = text_texture->height;
|
|
|
|
|
SDL_RenderCopy(renderer, text_texture->texture, NULL, &dst);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
|
|
|
|
|
set_draw_color(renderer, color);
|
|
|
|
|
SDL_RenderFillRect(renderer, rect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void stroke_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color) {
|
|
|
|
|
set_draw_color(renderer, color);
|
|
|
|
|
SDL_RenderDrawRect(renderer, rect);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static SDL_Rect fit_rect(int outer_x, int outer_y, int outer_w, int outer_h, int inner_w, int inner_h) {
|
|
|
|
|
SDL_Rect dst = {outer_x, outer_y, outer_w, outer_h};
|
|
|
|
|
double scale;
|
|
|
|
|
|
|
|
|
|
if (inner_w <= 0 || inner_h <= 0) {
|
|
|
|
|
return dst;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
scale = SDL_min((double) outer_w / (double) inner_w, (double) outer_h / (double) inner_h);
|
|
|
|
|
dst.w = (int) (inner_w * scale);
|
|
|
|
|
dst.h = (int) (inner_h * scale);
|
|
|
|
|
dst.x = outer_x + (outer_w - dst.w) / 2;
|
|
|
|
|
dst.y = outer_y + (outer_h - dst.h) / 2;
|
|
|
|
|
return dst;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void draw_video(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height, SDL_Rect destination) {
|
|
|
|
|
if (!video_texture) {
|
|
|
|
|
fill_rect(renderer, &destination, COLOR_BLACK);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
SDL_Rect fitted = fit_rect(destination.x, destination.y, destination.w, destination.h, texture_width, texture_height);
|
|
|
|
|
fill_rect(renderer, &destination, COLOR_BLACK);
|
|
|
|
|
SDL_RenderCopy(renderer, video_texture, NULL, &fitted);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void format_clock_label(char *buffer, size_t buffer_size, time_t now, int minute_offset) {
|
|
|
|
|
struct tm local_time;
|
|
|
|
|
time_t slot = now + minute_offset * 60;
|
|
|
|
|
localtime_r(&slot, &local_time);
|
|
|
|
|
strftime(buffer, buffer_size, "%I:%M %p", &local_time);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
static void draw_timeline_header_cached(SDL_Renderer *renderer, const UiCache *cache, SDL_Rect rect) {
|
2026-03-27 21:45:40 -04:00
|
|
|
int segments = 4;
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->timeline_day, rect.x + 12, rect.y + 8);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
for (int i = 0; i < segments; ++i) {
|
|
|
|
|
int x = rect.x + (rect.w * i) / segments;
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->timeline_labels[i], x + 6, rect.y + 10);
|
2026-03-27 21:45:40 -04:00
|
|
|
set_draw_color(renderer, COLOR_SLATE);
|
|
|
|
|
SDL_RenderDrawLine(renderer, x, rect.y + rect.h - 4, x, rect.y + rect.h + 5 * 76);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
static void draw_footer_legend(SDL_Renderer *renderer, const UiCache *cache, int window_width, int window_height) {
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_Rect footer = {0, window_height - 54, window_width, 54};
|
|
|
|
|
SDL_Rect chip = {window_width / 2 - 170, window_height - 40, 24, 24};
|
|
|
|
|
|
|
|
|
|
fill_rect(renderer, &footer, COLOR_PALE_BLUE);
|
|
|
|
|
set_draw_color(renderer, COLOR_SLATE);
|
|
|
|
|
SDL_RenderDrawLine(renderer, footer.x, footer.y, footer.x + footer.w, footer.y);
|
|
|
|
|
|
|
|
|
|
fill_rect(renderer, &chip, COLOR_HIGHLIGHT_YELLOW);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->footer_a, chip.x + 7, chip.y + 2);
|
|
|
|
|
draw_cached_text(renderer, &cache->footer_time, chip.x + 34, chip.y + 1);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
chip.x += 132;
|
|
|
|
|
fill_rect(renderer, &chip, COLOR_DEEP_BLUE);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->footer_b, chip.x + 7, chip.y + 2);
|
|
|
|
|
draw_cached_text(renderer, &cache->footer_theme, chip.x + 34, chip.y + 1);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
chip.x += 136;
|
|
|
|
|
fill_rect(renderer, &chip, COLOR_HINT_RED);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->footer_c, chip.x + 7, chip.y + 2);
|
|
|
|
|
draw_cached_text(renderer, &cache->footer_title, chip.x + 34, chip.y + 1);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
void ui_render_no_media(SDL_Renderer *renderer, const UiCache *cache) {
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
|
|
|
|
|
fill_rect(renderer, &full, COLOR_NAVY_DARK);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->no_media_title, 58, 80);
|
|
|
|
|
draw_cached_text(renderer, &cache->no_media_body, 58, 136);
|
|
|
|
|
draw_cached_text(renderer, &cache->no_media_hint, 58, 176);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ui_render_fullscreen(SDL_Renderer *renderer, SDL_Texture *video_texture, int texture_width, int texture_height) {
|
|
|
|
|
SDL_Rect window = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
|
|
|
|
|
fill_rect(renderer, &window, COLOR_BLACK);
|
|
|
|
|
draw_video(renderer, video_texture, texture_width, texture_height, window);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ui_render_guide(SDL_Renderer *renderer,
|
|
|
|
|
SDL_Texture *video_texture,
|
|
|
|
|
int texture_width,
|
|
|
|
|
int texture_height,
|
|
|
|
|
const UiFonts *fonts,
|
2026-03-27 23:19:42 -04:00
|
|
|
UiCache *cache,
|
2026-03-27 21:45:40 -04:00
|
|
|
const ChannelList *channels,
|
|
|
|
|
int active_channel,
|
2026-03-27 23:19:42 -04:00
|
|
|
Uint64 app_start_ticks,
|
|
|
|
|
Uint64 now_ticks,
|
|
|
|
|
time_t now_wall) {
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_Rect full = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};
|
|
|
|
|
SDL_Rect header_card = {0, 0, 390, 168};
|
|
|
|
|
SDL_Rect preview = {820, 0, 460, 210};
|
|
|
|
|
SDL_Rect header_row = {0, 210, WINDOW_WIDTH, 42};
|
|
|
|
|
SDL_Rect grid = {0, 252, WINDOW_WIDTH, WINDOW_HEIGHT - 306};
|
|
|
|
|
int row_height = grid.h / GUIDE_VISIBLE_ROWS;
|
|
|
|
|
int sidebar_width = 240;
|
|
|
|
|
int timeline_x = sidebar_width + 8;
|
|
|
|
|
int timeline_w = WINDOW_WIDTH - timeline_x - 12;
|
|
|
|
|
int start_index = active_channel - 2;
|
|
|
|
|
char detail[256];
|
|
|
|
|
|
|
|
|
|
fill_rect(renderer, &full, COLOR_NAVY_DARK);
|
|
|
|
|
fill_rect(renderer, &header_card, COLOR_PALE_BLUE);
|
|
|
|
|
fill_rect(renderer, &preview, COLOR_BLACK);
|
|
|
|
|
draw_video(renderer, video_texture, texture_width, texture_height, preview);
|
|
|
|
|
fill_rect(renderer, &header_row, COLOR_SLATE);
|
|
|
|
|
|
|
|
|
|
if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) {
|
|
|
|
|
const Channel *current = &channels->items[active_channel];
|
|
|
|
|
snprintf(detail, sizeof(detail), "%.0f min - %s", current->duration_seconds / 60.0, current->file_name);
|
|
|
|
|
fill_rect(renderer, &(SDL_Rect){0, 0, 390, 38}, COLOR_HIGHLIGHT_YELLOW);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->channels[active_channel].name_medium, 26, 52);
|
|
|
|
|
draw_cached_text(renderer, &cache->channels[active_channel].program_medium_dark, 20, 92);
|
|
|
|
|
draw_cached_text(renderer, &cache->channels[active_channel].detail_small_dark, 20, 124);
|
|
|
|
|
draw_cached_text(renderer, &cache->preview_badge, preview.x + preview.w - 126, preview.y + preview.h - 34);
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
if (cache->timeline_label_slot != now_wall / 60) {
|
|
|
|
|
char label[32];
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
format_clock_label(label, sizeof(label), now_wall, (int) ((TIMELINE_VISIBLE_SECONDS / 60.0 / 4) * i));
|
|
|
|
|
text_texture_destroy(&cache->timeline_labels[i]);
|
|
|
|
|
text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_DARK);
|
|
|
|
|
}
|
|
|
|
|
cache->timeline_label_slot = now_wall / 60;
|
|
|
|
|
}
|
|
|
|
|
draw_timeline_header_cached(renderer, cache, header_row);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
if (start_index < 0) {
|
|
|
|
|
start_index = 0;
|
|
|
|
|
}
|
|
|
|
|
if (channels && start_index + GUIDE_VISIBLE_ROWS > channels->count) {
|
|
|
|
|
start_index = SDL_max(0, channels->count - GUIDE_VISIBLE_ROWS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int row = 0; row < GUIDE_VISIBLE_ROWS; ++row) {
|
|
|
|
|
int channel_index = start_index + row;
|
|
|
|
|
SDL_Rect row_rect = {0, grid.y + row * row_height, WINDOW_WIDTH, row_height};
|
|
|
|
|
SDL_Rect sidebar = {0, row_rect.y, sidebar_width, row_height};
|
|
|
|
|
SDL_Rect timeline_rect = {timeline_x, row_rect.y + 6, timeline_w, row_height - 12};
|
|
|
|
|
SDL_Rect clip = timeline_rect;
|
|
|
|
|
int is_selected = channel_index == active_channel;
|
|
|
|
|
|
|
|
|
|
fill_rect(renderer, &row_rect, is_selected ? COLOR_DEEP_BLUE : COLOR_NAVY_DARK);
|
|
|
|
|
fill_rect(renderer, &sidebar, is_selected ? COLOR_DEEP_BLUE : COLOR_SLATE);
|
|
|
|
|
set_draw_color(renderer, COLOR_PALE_BLUE);
|
|
|
|
|
SDL_RenderDrawLine(renderer, row_rect.x, row_rect.y, row_rect.x + row_rect.w, row_rect.y);
|
|
|
|
|
|
|
|
|
|
if (!channels || channel_index >= channels->count) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
const Channel *channel = &channels->items[channel_index];
|
2026-03-27 23:19:42 -04:00
|
|
|
double live_position = channel_live_position_precise(channel, app_start_ticks, now_ticks);
|
2026-03-27 21:45:40 -04:00
|
|
|
double pixels_per_second = timeline_w / TIMELINE_VISIBLE_SECONDS;
|
|
|
|
|
int block_x = timeline_x - (int) (live_position * pixels_per_second);
|
|
|
|
|
int block_w = (int) (channel->duration_seconds * pixels_per_second);
|
|
|
|
|
SDL_Rect block = {block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8};
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer, &cache->channels[channel_index].name_medium, 20, row_rect.y + 12);
|
|
|
|
|
draw_cached_text(renderer, &cache->channels[channel_index].number_medium, 176, row_rect.y + 12);
|
|
|
|
|
draw_cached_text(renderer, &cache->channels[channel_index].file_small, 20, row_rect.y + 38);
|
2026-03-27 21:45:40 -04:00
|
|
|
|
|
|
|
|
fill_rect(renderer, &timeline_rect, COLOR_NAVY_DARK);
|
|
|
|
|
SDL_RenderSetClipRect(renderer, &clip);
|
|
|
|
|
fill_rect(renderer, &block, is_selected ? COLOR_HIGHLIGHT_YELLOW : COLOR_DEEP_BLUE);
|
|
|
|
|
stroke_rect(renderer, &block, COLOR_PALE_BLUE);
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_cached_text(renderer,
|
|
|
|
|
is_selected ? &cache->channels[channel_index].program_medium_dark
|
|
|
|
|
: &cache->channels[channel_index].program_small_light,
|
|
|
|
|
block.x + 10,
|
|
|
|
|
block.y + 10);
|
2026-03-27 21:45:40 -04:00
|
|
|
SDL_RenderSetClipRect(renderer, NULL);
|
|
|
|
|
stroke_rect(renderer, &timeline_rect, COLOR_SLATE);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 23:19:42 -04:00
|
|
|
draw_footer_legend(renderer, cache, WINDOW_WIDTH, WINDOW_HEIGHT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ui_cache_init(UiCache *cache, SDL_Renderer *renderer, const UiFonts *fonts, const ChannelList *channels) {
|
|
|
|
|
char buffer[256];
|
|
|
|
|
|
|
|
|
|
if (!cache || !renderer || !fonts) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(cache, 0, sizeof(*cache));
|
|
|
|
|
cache->renderer = renderer;
|
|
|
|
|
cache->timeline_label_slot = -1;
|
|
|
|
|
cache->timeline_labels = calloc(4, sizeof(UiTextTexture));
|
|
|
|
|
if (!cache->timeline_labels) {
|
|
|
|
|
ui_cache_destroy(cache);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (text_texture_init(&cache->no_media_title, renderer, fonts->large, "Passport-C Media Player", COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->no_media_body, renderer, fonts->medium, "No channels found in ./media", COLOR_HIGHLIGHT_YELLOW) != 0 ||
|
|
|
|
|
text_texture_init(&cache->no_media_hint, renderer, fonts->small, "Add MP4 or MKV files to ./media and relaunch.", COLOR_PALE_BLUE) != 0 ||
|
|
|
|
|
text_texture_init(&cache->timeline_day, renderer, fonts->medium, "FRI 6/30", COLOR_TEXT_DARK) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_a, renderer, fonts->medium, "A", COLOR_TEXT_DARK) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_time, renderer, fonts->medium, "Time", COLOR_TEXT_DARK) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_b, renderer, fonts->medium, "B", COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_theme, renderer, fonts->medium, "Theme", COLOR_TEXT_DARK) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_c, renderer, fonts->medium, "C", COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->footer_title, renderer, fonts->medium, "Title", COLOR_TEXT_DARK) != 0 ||
|
|
|
|
|
text_texture_init(&cache->preview_badge, renderer, fonts->medium, "Nick * 56", COLOR_TEXT_LIGHT) != 0) {
|
|
|
|
|
ui_cache_destroy(cache);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (channels && channels->count > 0) {
|
|
|
|
|
cache->channels = calloc((size_t) channels->count, sizeof(UiChannelCache));
|
|
|
|
|
if (!cache->channels) {
|
|
|
|
|
ui_cache_destroy(cache);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
cache->channel_count = channels->count;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < channels->count; ++i) {
|
|
|
|
|
const Channel *channel = &channels->items[i];
|
|
|
|
|
snprintf(buffer, sizeof(buffer), "%d", channel->number);
|
|
|
|
|
if (text_texture_init(&cache->channels[i].name_medium, renderer, fonts->medium, channel->name, COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->channels[i].number_medium, renderer, fonts->medium, buffer, COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->channels[i].file_small, renderer, fonts->small, channel->file_name, COLOR_PALE_BLUE) != 0 ||
|
|
|
|
|
text_texture_init(&cache->channels[i].program_small_light, renderer, fonts->small, channel->program_title, COLOR_TEXT_LIGHT) != 0 ||
|
|
|
|
|
text_texture_init(&cache->channels[i].program_medium_dark, renderer, fonts->medium, channel->program_title, COLOR_TEXT_DARK) != 0) {
|
|
|
|
|
ui_cache_destroy(cache);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
snprintf(buffer, sizeof(buffer), "%.0f min - %s", channel->duration_seconds / 60.0, channel->file_name);
|
|
|
|
|
if (text_texture_init(&cache->channels[i].detail_small_dark, renderer, fonts->small, buffer, COLOR_TEXT_DARK) != 0) {
|
|
|
|
|
ui_cache_destroy(cache);
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ui_cache_destroy(UiCache *cache) {
|
|
|
|
|
if (!cache) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text_texture_destroy(&cache->no_media_title);
|
|
|
|
|
text_texture_destroy(&cache->no_media_body);
|
|
|
|
|
text_texture_destroy(&cache->no_media_hint);
|
|
|
|
|
text_texture_destroy(&cache->timeline_day);
|
|
|
|
|
text_texture_destroy(&cache->footer_a);
|
|
|
|
|
text_texture_destroy(&cache->footer_time);
|
|
|
|
|
text_texture_destroy(&cache->footer_b);
|
|
|
|
|
text_texture_destroy(&cache->footer_theme);
|
|
|
|
|
text_texture_destroy(&cache->footer_c);
|
|
|
|
|
text_texture_destroy(&cache->footer_title);
|
|
|
|
|
text_texture_destroy(&cache->preview_badge);
|
|
|
|
|
|
|
|
|
|
if (cache->timeline_labels) {
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
text_texture_destroy(&cache->timeline_labels[i]);
|
|
|
|
|
}
|
|
|
|
|
free(cache->timeline_labels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cache->channels) {
|
|
|
|
|
for (int i = 0; i < cache->channel_count; ++i) {
|
|
|
|
|
text_texture_destroy(&cache->channels[i].name_medium);
|
|
|
|
|
text_texture_destroy(&cache->channels[i].number_medium);
|
|
|
|
|
text_texture_destroy(&cache->channels[i].file_small);
|
|
|
|
|
text_texture_destroy(&cache->channels[i].program_small_light);
|
|
|
|
|
text_texture_destroy(&cache->channels[i].program_medium_dark);
|
|
|
|
|
text_texture_destroy(&cache->channels[i].detail_small_dark);
|
|
|
|
|
}
|
|
|
|
|
free(cache->channels);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(cache, 0, sizeof(*cache));
|
2026-03-27 21:45:40 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ui_load_fonts(UiFonts *fonts) {
|
|
|
|
|
if (!fonts) {
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
memset(fonts, 0, sizeof(*fonts));
|
|
|
|
|
for (size_t i = 0; i < sizeof(FONT_CANDIDATES) / sizeof(FONT_CANDIDATES[0]); ++i) {
|
|
|
|
|
fonts->small = TTF_OpenFont(FONT_CANDIDATES[i], 18);
|
|
|
|
|
fonts->medium = TTF_OpenFont(FONT_CANDIDATES[i], 24);
|
|
|
|
|
fonts->large = TTF_OpenFont(FONT_CANDIDATES[i], 32);
|
|
|
|
|
if (fonts->small && fonts->medium && fonts->large) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
ui_destroy_fonts(fonts);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ui_destroy_fonts(UiFonts *fonts) {
|
|
|
|
|
if (!fonts) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fonts->small) {
|
|
|
|
|
TTF_CloseFont(fonts->small);
|
|
|
|
|
}
|
|
|
|
|
if (fonts->medium) {
|
|
|
|
|
TTF_CloseFont(fonts->medium);
|
|
|
|
|
}
|
|
|
|
|
if (fonts->large) {
|
|
|
|
|
TTF_CloseFont(fonts->large);
|
|
|
|
|
}
|
|
|
|
|
memset(fonts, 0, sizeof(*fonts));
|
|
|
|
|
}
|