Add channel banner when switching channels
This commit is contained in:
parent
8890d7f527
commit
be4e8b768f
9 changed files with 358 additions and 70 deletions
45
src/app.c
45
src/app.c
|
|
@ -9,6 +9,11 @@
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#define GUIDE_BROWSE_STEP_MINUTES 30
|
||||||
|
#define GUIDE_BROWSE_MAX_AHEAD_MINUTES (12 * 60)
|
||||||
|
#define GUIDE_BROWSE_MAX_OFFSET_MINUTES (GUIDE_BROWSE_MAX_AHEAD_MINUTES - ((int) (TIMELINE_VISIBLE_SECONDS / 60.0) - 30))
|
||||||
|
#define CHANNEL_BANNER_DURATION_MS 3000
|
||||||
|
|
||||||
static void configure_runtime_environment(void) {
|
static void configure_runtime_environment(void) {
|
||||||
char runtime_dir[64];
|
char runtime_dir[64];
|
||||||
char pulse_socket[96];
|
char pulse_socket[96];
|
||||||
|
|
@ -135,6 +140,24 @@ static void tune_relative(App *app, int delta) {
|
||||||
destroy_video_texture(app);
|
destroy_video_texture(app);
|
||||||
begin_startup_handoff(app);
|
begin_startup_handoff(app);
|
||||||
player_tune(&app->player, next_index);
|
player_tune(&app->player, next_index);
|
||||||
|
app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void browse_guide_time(App *app, int delta_minutes) {
|
||||||
|
int next_offset;
|
||||||
|
|
||||||
|
if (!app || app->mode != MODE_GUIDE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next_offset = app->guide_time_offset_minutes + delta_minutes;
|
||||||
|
if (next_offset < 0) {
|
||||||
|
next_offset = 0;
|
||||||
|
}
|
||||||
|
if (next_offset > GUIDE_BROWSE_MAX_OFFSET_MINUTES) {
|
||||||
|
next_offset = GUIDE_BROWSE_MAX_OFFSET_MINUTES;
|
||||||
|
}
|
||||||
|
app->guide_time_offset_minutes = next_offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void toggle_fullscreen(App *app) {
|
static void toggle_fullscreen(App *app) {
|
||||||
|
|
@ -230,6 +253,12 @@ static void handle_event(App *app, const SDL_Event *event) {
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
tune_relative(app, 1);
|
tune_relative(app, 1);
|
||||||
break;
|
break;
|
||||||
|
case SDLK_LEFT:
|
||||||
|
browse_guide_time(app, -GUIDE_BROWSE_STEP_MINUTES);
|
||||||
|
break;
|
||||||
|
case SDLK_RIGHT:
|
||||||
|
browse_guide_time(app, GUIDE_BROWSE_STEP_MINUTES);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -300,6 +329,7 @@ int app_init(App *app) {
|
||||||
if (app->channels.count > 0) {
|
if (app->channels.count > 0) {
|
||||||
begin_startup_handoff(app);
|
begin_startup_handoff(app);
|
||||||
player_tune(&app->player, 0);
|
player_tune(&app->player, 0);
|
||||||
|
app->channel_banner_until = SDL_GetTicks() + CHANNEL_BANNER_DURATION_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|
@ -356,9 +386,9 @@ void app_run(App *app) {
|
||||||
&app->ui_cache,
|
&app->ui_cache,
|
||||||
&app->channels,
|
&app->channels,
|
||||||
app->player.current_index,
|
app->player.current_index,
|
||||||
app->app_start_ticks,
|
app->app_start_time,
|
||||||
now_ticks,
|
now_wall,
|
||||||
now_wall);
|
app->guide_time_offset_minutes);
|
||||||
if (app->theme_picker_open) {
|
if (app->theme_picker_open) {
|
||||||
ui_render_theme_picker(app->renderer,
|
ui_render_theme_picker(app->renderer,
|
||||||
&app->fonts,
|
&app->fonts,
|
||||||
|
|
@ -383,7 +413,14 @@ void app_run(App *app) {
|
||||||
app->texture_width,
|
app->texture_width,
|
||||||
app->texture_height,
|
app->texture_height,
|
||||||
output_width,
|
output_width,
|
||||||
output_height);
|
output_height,
|
||||||
|
&GUIDE_THEMES[app->theme_index],
|
||||||
|
&app->fonts,
|
||||||
|
&app->channels,
|
||||||
|
app->player.current_index,
|
||||||
|
app->app_start_time,
|
||||||
|
now_wall,
|
||||||
|
SDL_GetTicks() < app->channel_banner_until);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_RenderPresent(app->renderer);
|
SDL_RenderPresent(app->renderer);
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,10 @@ typedef struct App {
|
||||||
int startup_handoff_active;
|
int startup_handoff_active;
|
||||||
int last_blackout_state;
|
int last_blackout_state;
|
||||||
Uint32 startup_handoff_until;
|
Uint32 startup_handoff_until;
|
||||||
|
Uint32 channel_banner_until;
|
||||||
time_t app_start_time;
|
time_t app_start_time;
|
||||||
Uint64 app_start_ticks;
|
Uint64 app_start_ticks;
|
||||||
|
int guide_time_offset_minutes;
|
||||||
ChannelList channels;
|
ChannelList channels;
|
||||||
Player player;
|
Player player;
|
||||||
UiFonts fonts;
|
UiFonts fonts;
|
||||||
|
|
|
||||||
BIN
src/app.o
BIN
src/app.o
Binary file not shown.
|
|
@ -333,6 +333,16 @@ time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_tick
|
||||||
return app_start_time + (time_t) (elapsed_ticks / 1000);
|
return app_start_time + (time_t) (elapsed_ticks / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double channel_schedule_elapsed_seconds(time_t app_start_time, time_t target_time) {
|
||||||
|
double elapsed = difftime(target_time, app_start_time);
|
||||||
|
|
||||||
|
if (elapsed < 0.0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return elapsed;
|
||||||
|
}
|
||||||
|
|
||||||
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) {
|
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now) {
|
||||||
double elapsed;
|
double elapsed;
|
||||||
|
|
||||||
|
|
@ -363,16 +373,8 @@ double channel_live_position_precise(const Channel *channel, Uint64 app_start_ti
|
||||||
return fmod(elapsed, channel->total_duration_seconds);
|
return fmod(elapsed, channel->total_duration_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProgramEntry *channel_program_at_index(const Channel *channel, int program_index) {
|
const ProgramEntry *channel_resolve_program_at_elapsed(const Channel *channel,
|
||||||
if (!channel || program_index < 0 || program_index >= channel->program_count) {
|
double elapsed_seconds,
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return &channel->programs[program_index];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProgramEntry *channel_resolve_program(const Channel *channel,
|
|
||||||
Uint64 app_start_ticks,
|
|
||||||
Uint64 now_ticks,
|
|
||||||
double *program_seek_seconds,
|
double *program_seek_seconds,
|
||||||
int *program_index) {
|
int *program_index) {
|
||||||
double channel_offset;
|
double channel_offset;
|
||||||
|
|
@ -381,10 +383,23 @@ const ProgramEntry *channel_resolve_program(const Channel *channel,
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel_offset = channel_live_position_precise(channel, app_start_ticks, now_ticks);
|
if (elapsed_seconds < 0.0) {
|
||||||
|
elapsed_seconds = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel_offset = fmod(elapsed_seconds, channel->total_duration_seconds);
|
||||||
|
if (channel_offset < 0.0) {
|
||||||
|
channel_offset += channel->total_duration_seconds;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < channel->program_count; ++i) {
|
for (int i = 0; i < channel->program_count; ++i) {
|
||||||
const ProgramEntry *program = &channel->programs[i];
|
const ProgramEntry *program = &channel->programs[i];
|
||||||
double end_offset = program->start_offset_seconds + program->duration_seconds;
|
double end_offset = program->start_offset_seconds + program->duration_seconds;
|
||||||
|
|
||||||
|
if (program->duration_seconds <= 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (channel_offset < end_offset || i == channel->program_count - 1) {
|
if (channel_offset < end_offset || i == channel->program_count - 1) {
|
||||||
if (program_seek_seconds) {
|
if (program_seek_seconds) {
|
||||||
*program_seek_seconds = channel_offset - program->start_offset_seconds;
|
*program_seek_seconds = channel_offset - program->start_offset_seconds;
|
||||||
|
|
@ -401,3 +416,29 @@ const ProgramEntry *channel_resolve_program(const Channel *channel,
|
||||||
|
|
||||||
return &channel->programs[0];
|
return &channel->programs[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ProgramEntry *channel_program_at_index(const Channel *channel, int program_index) {
|
||||||
|
if (!channel || program_index < 0 || program_index >= channel->program_count) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return &channel->programs[program_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProgramEntry *channel_resolve_program(const Channel *channel,
|
||||||
|
Uint64 app_start_ticks,
|
||||||
|
Uint64 now_ticks,
|
||||||
|
double *program_seek_seconds,
|
||||||
|
int *program_index) {
|
||||||
|
double elapsed;
|
||||||
|
|
||||||
|
if (!channel || channel->program_count == 0 || channel->total_duration_seconds <= 0.0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (now_ticks < app_start_ticks) {
|
||||||
|
now_ticks = app_start_ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed = (double) (now_ticks - app_start_ticks) / 1000.0;
|
||||||
|
return channel_resolve_program_at_elapsed(channel, elapsed, program_seek_seconds, program_index);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,13 @@ typedef struct ChannelList {
|
||||||
int channel_list_load(ChannelList *list, const char *media_dir);
|
int channel_list_load(ChannelList *list, const char *media_dir);
|
||||||
void channel_list_destroy(ChannelList *list);
|
void channel_list_destroy(ChannelList *list);
|
||||||
time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_ticks, Uint64 now_ticks);
|
time_t channel_wall_time_from_ticks(time_t app_start_time, Uint64 app_start_ticks, Uint64 now_ticks);
|
||||||
|
double channel_schedule_elapsed_seconds(time_t app_start_time, time_t target_time);
|
||||||
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now);
|
double channel_live_position(const Channel *channel, time_t app_start_time, time_t now);
|
||||||
double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks);
|
double channel_live_position_precise(const Channel *channel, Uint64 app_start_ticks, Uint64 now_ticks);
|
||||||
|
const ProgramEntry *channel_resolve_program_at_elapsed(const Channel *channel,
|
||||||
|
double elapsed_seconds,
|
||||||
|
double *program_seek_seconds,
|
||||||
|
int *program_index);
|
||||||
const ProgramEntry *channel_resolve_program(const Channel *channel,
|
const ProgramEntry *channel_resolve_program(const Channel *channel,
|
||||||
Uint64 app_start_ticks,
|
Uint64 app_start_ticks,
|
||||||
Uint64 now_ticks,
|
Uint64 now_ticks,
|
||||||
|
|
|
||||||
BIN
src/channel.o
BIN
src/channel.o
Binary file not shown.
260
src/ui.c
260
src/ui.c
|
|
@ -12,6 +12,7 @@ static const char *FONT_CANDIDATES[] = {
|
||||||
};
|
};
|
||||||
|
|
||||||
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
|
static void fill_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
|
||||||
|
static void stroke_rect(SDL_Renderer *renderer, const SDL_Rect *rect, SDL_Color color);
|
||||||
static SDL_Texture *text_to_texture(SDL_Renderer *renderer, TTF_Font *font, const char *text, SDL_Color color, int *width, int *height);
|
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) {
|
static void text_texture_destroy(UiTextTexture *text_texture) {
|
||||||
|
|
@ -235,6 +236,37 @@ static void draw_beveled_bar(SDL_Renderer *renderer,
|
||||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void draw_program_block(SDL_Renderer *renderer,
|
||||||
|
const SDL_Rect *rect,
|
||||||
|
SDL_Color top,
|
||||||
|
SDL_Color mid,
|
||||||
|
SDL_Color bottom,
|
||||||
|
SDL_Color gloss,
|
||||||
|
SDL_Color border,
|
||||||
|
int is_selected,
|
||||||
|
SDL_Color selection_edge) {
|
||||||
|
SDL_Rect inner;
|
||||||
|
|
||||||
|
if (!rect || rect->w <= 2 || rect->h <= 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_beveled_bar(renderer, rect, top, mid, bottom, gloss, border);
|
||||||
|
stroke_rect(renderer, rect, border);
|
||||||
|
|
||||||
|
inner = (SDL_Rect){rect->x + 1, rect->y + 1, rect->w - 2, rect->h - 2};
|
||||||
|
if (inner.w > 1 && inner.h > 1) {
|
||||||
|
stroke_rect_alpha(renderer, &inner, color_with_alpha(COLOR_TEXT_LIGHT, is_selected ? 64 : 42));
|
||||||
|
fill_rect_alpha(renderer,
|
||||||
|
&(SDL_Rect){inner.x + 1, inner.y + 1, SDL_max(inner.w - 2, 0), 2},
|
||||||
|
color_with_alpha(COLOR_TEXT_LIGHT, is_selected ? 34 : 20));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_selected) {
|
||||||
|
draw_selection_glow(renderer, rect, selection_edge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void draw_pill_button(SDL_Renderer *renderer, const GuideTheme *theme, const SDL_Rect *rect, SDL_Color fill, SDL_Color border) {
|
static void draw_pill_button(SDL_Renderer *renderer, const GuideTheme *theme, const SDL_Rect *rect, SDL_Color fill, SDL_Color border) {
|
||||||
SDL_Rect shadow;
|
SDL_Rect shadow;
|
||||||
|
|
||||||
|
|
@ -468,9 +500,8 @@ static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
const GuideTheme *theme,
|
const GuideTheme *theme,
|
||||||
const Channel *selected_channel,
|
const Channel *selected_channel,
|
||||||
const SDL_Rect *rect,
|
const SDL_Rect *rect,
|
||||||
Uint64 app_start_ticks,
|
time_t app_start_time,
|
||||||
Uint64 now_ticks,
|
time_t guide_focus_time) {
|
||||||
time_t now_wall) {
|
|
||||||
SDL_Rect accent;
|
SDL_Rect accent;
|
||||||
SDL_Rect clip_rect;
|
SDL_Rect clip_rect;
|
||||||
char time_range[64];
|
char time_range[64];
|
||||||
|
|
@ -495,11 +526,14 @@ static void draw_info_panel(SDL_Renderer *renderer,
|
||||||
draw_panel_bevel(renderer, rect, theme->gloss);
|
draw_panel_bevel(renderer, rect, theme->gloss);
|
||||||
stroke_rect(renderer, rect, theme->panel_border);
|
stroke_rect(renderer, rect, theme->panel_border);
|
||||||
|
|
||||||
program = channel_resolve_program(selected_channel, app_start_ticks, now_ticks, &program_seek, NULL);
|
program = channel_resolve_program_at_elapsed(selected_channel,
|
||||||
|
channel_schedule_elapsed_seconds(app_start_time, guide_focus_time),
|
||||||
|
&program_seek,
|
||||||
|
NULL);
|
||||||
if (!program) {
|
if (!program) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
start_time = now_wall - (time_t) program_seek;
|
start_time = guide_focus_time - (time_t) program_seek;
|
||||||
end_time = start_time + (time_t) program->duration_seconds;
|
end_time = start_time + (time_t) program->duration_seconds;
|
||||||
format_time_compact(time_range, sizeof(time_range), start_time);
|
format_time_compact(time_range, sizeof(time_range), start_time);
|
||||||
format_time_compact(end_text, sizeof(end_text), end_time);
|
format_time_compact(end_text, sizeof(end_text), end_time);
|
||||||
|
|
@ -554,7 +588,7 @@ static void draw_footer_legend(SDL_Renderer *renderer,
|
||||||
int window_width,
|
int window_width,
|
||||||
int window_height) {
|
int window_height) {
|
||||||
SDL_Rect footer = {0, window_height - 54, window_width, 54};
|
SDL_Rect footer = {0, window_height - 54, window_width, 54};
|
||||||
SDL_Rect chip = {window_width / 2 - 120, window_height - 38, 34, 20};
|
SDL_Rect chip = {window_width / 2 - 220, window_height - 38, 34, 20};
|
||||||
SDL_Color footer_text = readable_text_color(theme->footer_mid);
|
SDL_Color footer_text = readable_text_color(theme->footer_mid);
|
||||||
|
|
||||||
fill_three_stop_gradient(renderer,
|
fill_three_stop_gradient(renderer,
|
||||||
|
|
@ -573,6 +607,11 @@ static void draw_footer_legend(SDL_Renderer *renderer,
|
||||||
draw_pill_button(renderer, theme, &chip, theme->block_mid, theme->panel_border);
|
draw_pill_button(renderer, theme, &chip, theme->block_mid, theme->panel_border);
|
||||||
draw_text_clipped(renderer, fonts->small, "B", &footer, chip.x + 11, chip.y + 2, footer_text);
|
draw_text_clipped(renderer, fonts->small, "B", &footer, chip.x + 11, chip.y + 2, footer_text);
|
||||||
draw_text_clipped(renderer, fonts->small, "THEME", &footer, chip.x + 42, chip.y - 1, footer_text);
|
draw_text_clipped(renderer, fonts->small, "THEME", &footer, chip.x + 42, chip.y - 1, footer_text);
|
||||||
|
|
||||||
|
chip.x += 160;
|
||||||
|
draw_pill_button(renderer, theme, &chip, theme->row_mid, theme->panel_border);
|
||||||
|
draw_text_clipped(renderer, fonts->small, "<>", &footer, chip.x + 5, chip.y + 2, footer_text);
|
||||||
|
draw_text_clipped(renderer, fonts->small, "TIME", &footer, chip.x + 42, chip.y - 1, footer_text);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void draw_scanline_overlay(SDL_Renderer *renderer, int width, int height, const GuideTheme *theme) {
|
static void draw_scanline_overlay(SDL_Renderer *renderer, int width, int height, const GuideTheme *theme) {
|
||||||
|
|
@ -582,6 +621,100 @@ static void draw_scanline_overlay(SDL_Renderer *renderer, int width, int height,
|
||||||
(void) theme;
|
(void) theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void draw_channel_status_banner(SDL_Renderer *renderer,
|
||||||
|
const UiFonts *fonts,
|
||||||
|
const GuideTheme *theme,
|
||||||
|
const ChannelList *channels,
|
||||||
|
int active_channel,
|
||||||
|
time_t app_start_time,
|
||||||
|
time_t now_wall,
|
||||||
|
int window_width,
|
||||||
|
int window_height) {
|
||||||
|
SDL_Rect banner;
|
||||||
|
SDL_Rect channel_pill;
|
||||||
|
SDL_Rect info_clip;
|
||||||
|
SDL_Color banner_text;
|
||||||
|
SDL_Color sub_text;
|
||||||
|
char channel_text[96];
|
||||||
|
char time_range[64];
|
||||||
|
char end_text[32];
|
||||||
|
time_t start_time;
|
||||||
|
time_t end_time;
|
||||||
|
double program_seek = 0.0;
|
||||||
|
const Channel *channel;
|
||||||
|
const ProgramEntry *program;
|
||||||
|
|
||||||
|
if (!renderer || !fonts || !theme || !channels || active_channel < 0 || active_channel >= channels->count) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
channel = &channels->items[active_channel];
|
||||||
|
program = channel_resolve_program_at_elapsed(channel,
|
||||||
|
channel_schedule_elapsed_seconds(app_start_time, now_wall),
|
||||||
|
&program_seek,
|
||||||
|
NULL);
|
||||||
|
if (!program) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
banner = (SDL_Rect){window_width / 2 - 360, window_height - 92, 720, 64};
|
||||||
|
if (banner.x < 24) {
|
||||||
|
banner.x = 24;
|
||||||
|
banner.w = window_width - 48;
|
||||||
|
}
|
||||||
|
channel_pill = (SDL_Rect){banner.x + 10, banner.y + 10, 210, banner.h - 20};
|
||||||
|
info_clip = (SDL_Rect){channel_pill.x + channel_pill.w + 14, banner.y + 8, banner.w - channel_pill.w - 28, banner.h - 16};
|
||||||
|
banner_text = ensure_contrast(theme->ribbon_text, theme->status_mid);
|
||||||
|
sub_text = ensure_contrast(theme->row_subtext, theme->status_mid);
|
||||||
|
|
||||||
|
start_time = now_wall - (time_t) program_seek;
|
||||||
|
end_time = start_time + (time_t) program->duration_seconds;
|
||||||
|
format_time_compact(time_range, sizeof(time_range), start_time);
|
||||||
|
format_time_compact(end_text, sizeof(end_text), end_time);
|
||||||
|
strncat(time_range, " - ", sizeof(time_range) - strlen(time_range) - 1);
|
||||||
|
strncat(time_range, end_text, sizeof(time_range) - strlen(time_range) - 1);
|
||||||
|
snprintf(channel_text, sizeof(channel_text), "%s %d", channel->name, channel->number);
|
||||||
|
|
||||||
|
draw_panel_shadow(renderer, &banner);
|
||||||
|
draw_beveled_bar(renderer,
|
||||||
|
&banner,
|
||||||
|
blend_color(theme->status_top, theme->status_mid, 220),
|
||||||
|
theme->status_mid,
|
||||||
|
blend_color(theme->status_bottom, theme->status_mid, 220),
|
||||||
|
color_with_alpha(theme->gloss, 42),
|
||||||
|
theme->panel_border);
|
||||||
|
stroke_rect(renderer, &banner, theme->panel_border);
|
||||||
|
draw_pill_button(renderer, theme, &channel_pill, theme->panel_fill, theme->panel_border);
|
||||||
|
draw_text_shadowed(renderer,
|
||||||
|
fonts->medium,
|
||||||
|
channel_text,
|
||||||
|
&channel_pill,
|
||||||
|
channel_pill.x + 14,
|
||||||
|
channel_pill.y + 8,
|
||||||
|
ensure_contrast(theme->panel_text, theme->panel_fill),
|
||||||
|
color_with_alpha(COLOR_BLACK, 255));
|
||||||
|
set_draw_color(renderer, theme->status_divider);
|
||||||
|
SDL_RenderDrawLine(renderer,
|
||||||
|
channel_pill.x + channel_pill.w + 6,
|
||||||
|
banner.y + 10,
|
||||||
|
channel_pill.x + channel_pill.w + 6,
|
||||||
|
banner.y + banner.h - 10);
|
||||||
|
draw_text_clipped(renderer,
|
||||||
|
fonts->medium,
|
||||||
|
program->program_title,
|
||||||
|
&info_clip,
|
||||||
|
info_clip.x,
|
||||||
|
banner.y + 10,
|
||||||
|
banner_text);
|
||||||
|
draw_text_clipped(renderer,
|
||||||
|
fonts->small,
|
||||||
|
time_range,
|
||||||
|
&info_clip,
|
||||||
|
info_clip.x,
|
||||||
|
banner.y + 36,
|
||||||
|
sub_text);
|
||||||
|
}
|
||||||
|
|
||||||
void ui_render_about_modal(SDL_Renderer *renderer,
|
void ui_render_about_modal(SDL_Renderer *renderer,
|
||||||
const UiFonts *fonts,
|
const UiFonts *fonts,
|
||||||
int window_width,
|
int window_width,
|
||||||
|
|
@ -668,10 +801,28 @@ void ui_render_fullscreen(SDL_Renderer *renderer,
|
||||||
int texture_width,
|
int texture_width,
|
||||||
int texture_height,
|
int texture_height,
|
||||||
int window_width,
|
int window_width,
|
||||||
int window_height) {
|
int window_height,
|
||||||
|
const GuideTheme *theme,
|
||||||
|
const UiFonts *fonts,
|
||||||
|
const ChannelList *channels,
|
||||||
|
int active_channel,
|
||||||
|
time_t app_start_time,
|
||||||
|
time_t now_wall,
|
||||||
|
int show_channel_banner) {
|
||||||
SDL_Rect window = {0, 0, window_width, window_height};
|
SDL_Rect window = {0, 0, window_width, window_height};
|
||||||
fill_rect(renderer, &window, COLOR_BLACK);
|
fill_rect(renderer, &window, COLOR_BLACK);
|
||||||
draw_video(renderer, video_texture, texture_width, texture_height, window);
|
draw_video(renderer, video_texture, texture_width, texture_height, window);
|
||||||
|
if (show_channel_banner) {
|
||||||
|
draw_channel_status_banner(renderer,
|
||||||
|
fonts,
|
||||||
|
theme,
|
||||||
|
channels,
|
||||||
|
active_channel,
|
||||||
|
app_start_time,
|
||||||
|
now_wall,
|
||||||
|
window_width,
|
||||||
|
window_height);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ui_render_guide(SDL_Renderer *renderer,
|
void ui_render_guide(SDL_Renderer *renderer,
|
||||||
|
|
@ -685,9 +836,9 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
UiCache *cache,
|
UiCache *cache,
|
||||||
const ChannelList *channels,
|
const ChannelList *channels,
|
||||||
int active_channel,
|
int active_channel,
|
||||||
Uint64 app_start_ticks,
|
time_t app_start_time,
|
||||||
Uint64 now_ticks,
|
time_t now_wall,
|
||||||
time_t now_wall) {
|
int guide_time_offset_minutes) {
|
||||||
double scale_x = (double) window_width / WINDOW_WIDTH;
|
double scale_x = (double) window_width / WINDOW_WIDTH;
|
||||||
double scale_y = (double) window_height / WINDOW_HEIGHT;
|
double scale_y = (double) window_height / WINDOW_HEIGHT;
|
||||||
int guide_x_start = (int) (GUIDE_X_START * scale_x);
|
int guide_x_start = (int) (GUIDE_X_START * scale_x);
|
||||||
|
|
@ -703,7 +854,11 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
int start_index = active_channel - 2;
|
int start_index = active_channel - 2;
|
||||||
const Channel *selected_channel = NULL;
|
const Channel *selected_channel = NULL;
|
||||||
double pixels_per_minute = timeline_w / 90.0;
|
double pixels_per_minute = timeline_w / 90.0;
|
||||||
time_t guide_view_start_time = now_wall - (30 * 60);
|
time_t guide_view_start_time = now_wall - (30 * 60) + (guide_time_offset_minutes * 60);
|
||||||
|
time_t guide_focus_time = guide_view_start_time + (30 * 60);
|
||||||
|
double guide_view_start_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_view_start_time);
|
||||||
|
double guide_view_end_seconds = guide_view_start_seconds + TIMELINE_VISIBLE_SECONDS;
|
||||||
|
double guide_focus_seconds = channel_schedule_elapsed_seconds(app_start_time, guide_focus_time);
|
||||||
|
|
||||||
if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) {
|
if (channels && channels->count > 0 && active_channel >= 0 && active_channel < channels->count) {
|
||||||
selected_channel = &channels->items[active_channel];
|
selected_channel = &channels->items[active_channel];
|
||||||
|
|
@ -714,20 +869,20 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
theme->background_top,
|
theme->background_top,
|
||||||
theme->background_mid,
|
theme->background_mid,
|
||||||
theme->background_bottom);
|
theme->background_bottom);
|
||||||
draw_info_panel(renderer, fonts, theme, selected_channel, &info_panel, app_start_ticks, now_ticks, now_wall);
|
draw_info_panel(renderer, fonts, theme, selected_channel, &info_panel, app_start_time, guide_focus_time);
|
||||||
draw_panel_shadow(renderer, &preview);
|
draw_panel_shadow(renderer, &preview);
|
||||||
fill_rect(renderer, &preview, COLOR_BLACK);
|
fill_rect(renderer, &preview, COLOR_BLACK);
|
||||||
draw_video(renderer, video_texture, texture_width, texture_height, preview);
|
draw_video(renderer, video_texture, texture_width, texture_height, preview);
|
||||||
draw_status_bar(renderer, fonts->medium, theme, selected_channel, &status_bar, now_wall);
|
draw_status_bar(renderer, fonts->medium, theme, selected_channel, &status_bar, now_wall);
|
||||||
|
|
||||||
if (cache->timeline_label_slot != now_wall / 60 || cache->timeline_theme != theme) {
|
if (cache->timeline_label_slot != guide_view_start_time / 60 || cache->timeline_theme != theme) {
|
||||||
char label[32];
|
char label[32];
|
||||||
for (int i = 0; i < 4; ++i) {
|
for (int i = 0; i < 4; ++i) {
|
||||||
format_clock_label(label, sizeof(label), guide_view_start_time, 30 * i);
|
format_clock_label(label, sizeof(label), guide_view_start_time, 30 * i);
|
||||||
text_texture_destroy(&cache->timeline_labels[i]);
|
text_texture_destroy(&cache->timeline_labels[i]);
|
||||||
text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT);
|
text_texture_init(&cache->timeline_labels[i], renderer, fonts->small, label, COLOR_TEXT_LIGHT);
|
||||||
}
|
}
|
||||||
cache->timeline_label_slot = now_wall / 60;
|
cache->timeline_label_slot = guide_view_start_time / 60;
|
||||||
cache->timeline_theme = theme;
|
cache->timeline_theme = theme;
|
||||||
}
|
}
|
||||||
draw_timeline_header_cached(renderer,
|
draw_timeline_header_cached(renderer,
|
||||||
|
|
@ -778,14 +933,8 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
|
|
||||||
{
|
{
|
||||||
const Channel *channel = &channels->items[channel_index];
|
const Channel *channel = &channels->items[channel_index];
|
||||||
double program_seek = 0.0;
|
double focus_program_seek = 0.0;
|
||||||
const ProgramEntry *program = channel_resolve_program(channel, app_start_ticks, now_ticks, &program_seek, NULL);
|
const ProgramEntry *focus_program = channel_resolve_program_at_elapsed(channel, guide_focus_seconds, &focus_program_seek, NULL);
|
||||||
double guide_view_start_seconds = (double) guide_view_start_time;
|
|
||||||
double program_start_time = (double) now_wall;
|
|
||||||
int block_x = guide_x_start;
|
|
||||||
int block_w = 48;
|
|
||||||
SDL_Rect block = {guide_x_start, timeline_rect.y + 4, 48, timeline_rect.h - 8};
|
|
||||||
SDL_Rect title_rect = {block.x + 8, block.y + 8, block.w - 16, block.h - 16};
|
|
||||||
char title[128];
|
char title[128];
|
||||||
SDL_Color row_primary = ensure_contrast(is_selected ? theme->row_active_text : theme->row_text,
|
SDL_Color row_primary = ensure_contrast(is_selected ? theme->row_active_text : theme->row_text,
|
||||||
is_selected ? theme->row_active_mid : theme->row_mid);
|
is_selected ? theme->row_active_mid : theme->row_mid);
|
||||||
|
|
@ -797,14 +946,9 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
? color_with_alpha(COLOR_BLACK, 255)
|
? color_with_alpha(COLOR_BLACK, 255)
|
||||||
: color_with_alpha(COLOR_TEXT_LIGHT, 255);
|
: color_with_alpha(COLOR_TEXT_LIGHT, 255);
|
||||||
|
|
||||||
if (!program) {
|
if (!focus_program) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
program_start_time -= program_seek;
|
|
||||||
block_x = guide_x_start + (int) ((((program_start_time - guide_view_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
|
|
||||||
block_w = (int) ((program->duration_seconds / 60.0) * pixels_per_minute);
|
|
||||||
block = (SDL_Rect){block_x, timeline_rect.y + 4, SDL_max(block_w, 48), timeline_rect.h - 8};
|
|
||||||
title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16};
|
|
||||||
|
|
||||||
if (is_selected) {
|
if (is_selected) {
|
||||||
draw_text_shadowed(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
|
draw_text_shadowed(renderer, fonts->medium, channel->name, &sidebar, 20, row_rect.y + 12, row_primary, color_with_alpha(COLOR_BLACK, 255));
|
||||||
|
|
@ -823,7 +967,7 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
}
|
}
|
||||||
draw_text_shadowed(renderer,
|
draw_text_shadowed(renderer,
|
||||||
fonts->small,
|
fonts->small,
|
||||||
program->file_name,
|
focus_program->file_name,
|
||||||
&sidebar,
|
&sidebar,
|
||||||
20,
|
20,
|
||||||
row_rect.y + 38,
|
row_rect.y + 38,
|
||||||
|
|
@ -831,13 +975,61 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
color_with_alpha(COLOR_BLACK, 255));
|
color_with_alpha(COLOR_BLACK, 255));
|
||||||
|
|
||||||
SDL_RenderSetClipRect(renderer, &clip);
|
SDL_RenderSetClipRect(renderer, &clip);
|
||||||
draw_beveled_bar(renderer,
|
|
||||||
|
{
|
||||||
|
double block_cursor_seconds = guide_view_start_seconds;
|
||||||
|
int block_guard = channel->program_count;
|
||||||
|
|
||||||
|
if (channel->total_duration_seconds > 0.0) {
|
||||||
|
block_guard *= 1 + (int) (TIMELINE_VISIBLE_SECONDS / channel->total_duration_seconds);
|
||||||
|
}
|
||||||
|
block_guard += 8;
|
||||||
|
if (block_guard < 16) {
|
||||||
|
block_guard = 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (block_cursor_seconds < guide_view_end_seconds && block_guard-- > 0) {
|
||||||
|
double program_seek = 0.0;
|
||||||
|
const ProgramEntry *program = channel_resolve_program_at_elapsed(channel, block_cursor_seconds, &program_seek, NULL);
|
||||||
|
double program_start_seconds;
|
||||||
|
double program_end_seconds;
|
||||||
|
double visible_start_seconds;
|
||||||
|
double visible_end_seconds;
|
||||||
|
int block_x;
|
||||||
|
int block_w;
|
||||||
|
SDL_Rect block;
|
||||||
|
SDL_Rect title_rect;
|
||||||
|
|
||||||
|
if (!program || program->duration_seconds <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
program_start_seconds = block_cursor_seconds - program_seek;
|
||||||
|
program_end_seconds = program_start_seconds + program->duration_seconds;
|
||||||
|
visible_start_seconds = SDL_max(program_start_seconds, guide_view_start_seconds);
|
||||||
|
visible_end_seconds = SDL_min(program_end_seconds, guide_view_end_seconds);
|
||||||
|
|
||||||
|
block_cursor_seconds = program_end_seconds;
|
||||||
|
if (visible_end_seconds <= visible_start_seconds) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
block_x = guide_x_start + (int) ((((visible_start_seconds - guide_view_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
|
||||||
|
block_w = (int) ((((visible_end_seconds - visible_start_seconds) / 60.0) * pixels_per_minute) + 0.5);
|
||||||
|
block = (SDL_Rect){block_x + 1, timeline_rect.y + 4, SDL_max(block_w - 2, 22), timeline_rect.h - 8};
|
||||||
|
title_rect = (SDL_Rect){block.x + 8, block.y + 8, block.w - 16, block.h - 16};
|
||||||
|
|
||||||
|
draw_program_block(renderer,
|
||||||
&block,
|
&block,
|
||||||
is_selected ? theme->block_active_top : theme->block_top,
|
is_selected ? theme->block_active_top : theme->block_top,
|
||||||
is_selected ? theme->block_active_mid : theme->block_mid,
|
is_selected ? theme->block_active_mid : theme->block_mid,
|
||||||
is_selected ? theme->block_active_bottom : theme->block_bottom,
|
is_selected ? theme->block_active_bottom : theme->block_bottom,
|
||||||
is_selected ? theme->selection_edge : theme->gloss,
|
theme->gloss,
|
||||||
theme->panel_border);
|
theme->panel_border,
|
||||||
|
is_selected,
|
||||||
|
theme->selection_edge);
|
||||||
|
|
||||||
|
if (title_rect.w > 24) {
|
||||||
fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small,
|
fit_text_with_ellipsis(is_selected ? fonts->medium : fonts->small,
|
||||||
program->program_title,
|
program->program_title,
|
||||||
title_rect.w,
|
title_rect.w,
|
||||||
|
|
@ -851,6 +1043,10 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
title_rect.y,
|
title_rect.y,
|
||||||
block_text,
|
block_text,
|
||||||
block_shadow);
|
block_shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SDL_RenderSetClipRect(renderer, NULL);
|
SDL_RenderSetClipRect(renderer, NULL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
15
src/ui.h
15
src/ui.h
|
|
@ -52,7 +52,14 @@ void ui_render_fullscreen(SDL_Renderer *renderer,
|
||||||
int texture_width,
|
int texture_width,
|
||||||
int texture_height,
|
int texture_height,
|
||||||
int window_width,
|
int window_width,
|
||||||
int window_height);
|
int window_height,
|
||||||
|
const GuideTheme *theme,
|
||||||
|
const UiFonts *fonts,
|
||||||
|
const ChannelList *channels,
|
||||||
|
int active_channel,
|
||||||
|
time_t app_start_time,
|
||||||
|
time_t now_wall,
|
||||||
|
int show_channel_banner);
|
||||||
void ui_render_guide(SDL_Renderer *renderer,
|
void ui_render_guide(SDL_Renderer *renderer,
|
||||||
SDL_Texture *video_texture,
|
SDL_Texture *video_texture,
|
||||||
int texture_width,
|
int texture_width,
|
||||||
|
|
@ -64,9 +71,9 @@ void ui_render_guide(SDL_Renderer *renderer,
|
||||||
UiCache *cache,
|
UiCache *cache,
|
||||||
const ChannelList *channels,
|
const ChannelList *channels,
|
||||||
int active_channel,
|
int active_channel,
|
||||||
Uint64 app_start_ticks,
|
time_t app_start_time,
|
||||||
Uint64 now_ticks,
|
time_t now_wall,
|
||||||
time_t now_wall);
|
int guide_time_offset_minutes);
|
||||||
void ui_render_theme_picker(SDL_Renderer *renderer,
|
void ui_render_theme_picker(SDL_Renderer *renderer,
|
||||||
const UiFonts *fonts,
|
const UiFonts *fonts,
|
||||||
int window_width,
|
int window_width,
|
||||||
|
|
|
||||||
BIN
src/ui.o
BIN
src/ui.o
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue