mirror of
https://github.com/google/pebble.git
synced 2025-07-22 15:24:54 -04:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
238
src/fw/apps/system_apps/weather/weather_app.c
Normal file
238
src/fw/apps/system_apps/weather/weather_app.c
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "weather_app.h"
|
||||
#include "weather_app_layout.h"
|
||||
#include "weather_app_splash_screen.h"
|
||||
#include "weather_app_warning_dialog.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/event_service_client.h"
|
||||
#include "applib/ui/click.h"
|
||||
#include "applib/ui/content_indicator.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "kernel/events.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "process_management/pebble_process_md.h"
|
||||
#include "process_state/app_state/app_state.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/timeline/timeline.h"
|
||||
#include "services/normal/weather/weather_service.h"
|
||||
#include "services/normal/weather/weather_types.h"
|
||||
#include "util/array.h"
|
||||
#include "util/attributes.h"
|
||||
#include "util/list.h"
|
||||
#include "util/math.h"
|
||||
|
||||
typedef struct WeatherAppData {
|
||||
Window window;
|
||||
WeatherAppLayout layout;
|
||||
WeatherDataListNode *forecasts_list_head;
|
||||
size_t forecasts_count;
|
||||
unsigned int current_forecast_index;
|
||||
EventServiceInfo weather_event_info;
|
||||
WeatherAppWarningDialog *warning_dialog;
|
||||
} WeatherAppData;
|
||||
|
||||
static bool prv_is_weather_forecast_recent(WeatherLocationForecast *forecast) {
|
||||
if (!forecast) {
|
||||
return false;
|
||||
}
|
||||
const time_t current_time_utc = rtc_get_time();
|
||||
const int recent_threshold_seconds = 5 * SECONDS_PER_HOUR / 2; // 2.5 hours
|
||||
const int seconds_since_forecast_was_updated = current_time_utc - forecast->time_updated_utc;
|
||||
return (seconds_since_forecast_was_updated < recent_threshold_seconds);
|
||||
}
|
||||
|
||||
static void prv_warning_dialog_dismiss_cb(void) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
data->warning_dialog = NULL;
|
||||
}
|
||||
|
||||
static void prv_show_warning_dialog(WeatherAppData *data, bool exit_on_pop,
|
||||
const char *localized_text) {
|
||||
if (data->warning_dialog) {
|
||||
return; // only show one dialog at a time
|
||||
}
|
||||
if (exit_on_pop) {
|
||||
bool animated = false;
|
||||
app_window_stack_pop_all(animated);
|
||||
}
|
||||
data->warning_dialog = weather_app_warning_dialog_push(localized_text,
|
||||
prv_warning_dialog_dismiss_cb);
|
||||
}
|
||||
|
||||
static void prv_handle_weather(PebbleEvent *unused_event, void *unused_context) {
|
||||
// Unschedule any ongoing animations that would try to touch the weather data we're about to
|
||||
// update
|
||||
animation_unschedule_all();
|
||||
|
||||
size_t forecasts_count_out = 0;
|
||||
WeatherDataListNode *forecasts_list_head =
|
||||
weather_service_locations_list_create(&forecasts_count_out);
|
||||
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
weather_service_locations_list_destroy(data->forecasts_list_head);
|
||||
WeatherAppLayout *layout = &data->layout;
|
||||
if (forecasts_count_out > 0) {
|
||||
weather_app_layout_set_data(layout, &forecasts_list_head->forecast);
|
||||
const bool multiple_forecasts_exist = (forecasts_count_out > 1);
|
||||
weather_app_layout_set_down_arrow_visible(layout, multiple_forecasts_exist);
|
||||
|
||||
data->forecasts_list_head = forecasts_list_head;
|
||||
// Only show the first forecast if the number of forecasts has differed between fetches.
|
||||
// i.e. assume that the same number of forecasts means the locations have remained the same.
|
||||
if (data->forecasts_count != forecasts_count_out) {
|
||||
data->forecasts_count = forecasts_count_out;
|
||||
data->current_forecast_index = 0;
|
||||
}
|
||||
} else {
|
||||
/// Shown when there are no forecasts available to show the user
|
||||
const char *warning_text = i18n_get("No location information available. To see weather, add "\
|
||||
"locations in your Pebble mobile app.", data);
|
||||
const bool exit_on_pop = true;
|
||||
prv_show_warning_dialog(data, exit_on_pop, warning_text);
|
||||
weather_app_layout_set_down_arrow_visible(layout, false);
|
||||
weather_app_layout_set_data(layout, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_main_window_appear(Window *window) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
data->weather_event_info = (EventServiceInfo) {
|
||||
.type = PEBBLE_WEATHER_EVENT,
|
||||
.handler = prv_handle_weather,
|
||||
};
|
||||
event_service_client_subscribe(&data->weather_event_info);
|
||||
}
|
||||
|
||||
static void prv_main_window_load(Window *window) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
layer_add_child(&window->layer, &data->layout.root_layer);
|
||||
}
|
||||
|
||||
static void prv_main_window_disappear(Window *window) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
event_service_client_unsubscribe(&data->weather_event_info);
|
||||
}
|
||||
|
||||
static void prv_up_down_click_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
const bool not_enough_items_to_scroll = (data->forecasts_count <= 1);
|
||||
if (not_enough_items_to_scroll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_down_pressed = (click_recognizer_get_button_id(recognizer) == BUTTON_ID_DOWN);
|
||||
const int delta = is_down_pressed ? 1 : -1;
|
||||
data->current_forecast_index = positive_modulo(data->current_forecast_index + delta,
|
||||
data->forecasts_count);
|
||||
|
||||
WeatherDataListNode *node =
|
||||
weather_service_locations_list_get_location_at_index(data->forecasts_list_head,
|
||||
data->current_forecast_index);
|
||||
weather_app_layout_animate(&data->layout, &node->forecast, is_down_pressed);
|
||||
}
|
||||
|
||||
static void prv_main_window_click_provider(void *context) {
|
||||
window_single_click_subscribe(BUTTON_ID_UP, prv_up_down_click_handler);
|
||||
window_single_click_subscribe(BUTTON_ID_DOWN, prv_up_down_click_handler);
|
||||
}
|
||||
|
||||
static void prv_main_window_unload(Window *window) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
weather_app_layout_deinit(&data->layout);
|
||||
}
|
||||
|
||||
static NOINLINE void prv_init(void) {
|
||||
WeatherAppData *data = app_zalloc_check(sizeof(WeatherAppData));
|
||||
app_state_set_user_data(data);
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, WINDOW_NAME("Weather"));
|
||||
const WindowHandlers window_handlers = {
|
||||
.appear = prv_main_window_appear,
|
||||
.load = prv_main_window_load,
|
||||
.disappear = prv_main_window_disappear,
|
||||
.unload = prv_main_window_unload,
|
||||
};
|
||||
window_set_window_handlers(window, &window_handlers);
|
||||
|
||||
window_set_click_config_provider(window, prv_main_window_click_provider);
|
||||
window_set_user_data(window, data);
|
||||
|
||||
const GRect *layout_frame = &window->layer.bounds;
|
||||
WeatherAppLayout *layout = &data->layout;
|
||||
weather_app_layout_init(layout, layout_frame);
|
||||
window_set_user_data(window, layout);
|
||||
|
||||
// Fetch initial data
|
||||
prv_handle_weather(NULL, NULL);
|
||||
|
||||
if (data->forecasts_count == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool animated = true;
|
||||
app_window_stack_push(&data->window, animated);
|
||||
|
||||
// Request the default forecast separately instead of using the forecast list in `data` to avoid
|
||||
// any potential race conditions
|
||||
WeatherLocationForecast *default_forecast = weather_service_create_default_forecast();
|
||||
const bool is_default_forecast_data_recent = prv_is_weather_forecast_recent(default_forecast);
|
||||
weather_service_destroy_default_forecast(default_forecast);
|
||||
|
||||
// TODO PBL-38484: Consider using a different dialog for when data is stale but phone is connected
|
||||
if (is_default_forecast_data_recent || connection_service_peek_pebble_app_connection()) {
|
||||
const uint32_t splash_screen_timeout_ms = 500;
|
||||
weather_app_splash_screen_push(splash_screen_timeout_ms);
|
||||
} else {
|
||||
/// Shown when there is no connection to the phone and the data that we have is not recent
|
||||
const char *warning_text = i18n_get("Unable to connect. Your weather data may be out of date; "\
|
||||
"try checking the connection on your phone.", data);
|
||||
const bool exit_on_pop = false;
|
||||
prv_show_warning_dialog(data, exit_on_pop, warning_text);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_deinit(void) {
|
||||
WeatherAppData *data = app_state_get_user_data();
|
||||
i18n_free_all(data);
|
||||
}
|
||||
|
||||
static void prv_main(void) {
|
||||
prv_init();
|
||||
app_event_loop();
|
||||
prv_deinit();
|
||||
}
|
||||
|
||||
const PebbleProcessMd* weather_app_get_info() {
|
||||
const bool is_visible_in_launcher = weather_service_supported_by_phone();
|
||||
|
||||
static const PebbleProcessMdSystem s_weather_app_info = {
|
||||
.common = {
|
||||
.main_func = prv_main,
|
||||
.uuid = UUID_WEATHER_DATA_SOURCE,
|
||||
},
|
||||
.name = i18n_noop("Weather"),
|
||||
#if CAPABILITY_HAS_APP_GLANCES
|
||||
.icon_resource_id = RESOURCE_ID_GENERIC_WEATHER_TINY,
|
||||
#endif
|
||||
};
|
||||
|
||||
return is_visible_in_launcher ? (const PebbleProcessMd *)&s_weather_app_info : NULL;
|
||||
}
|
21
src/fw/apps/system_apps/weather/weather_app.h
Normal file
21
src/fw/apps/system_apps/weather/weather_app.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "process_management/pebble_process_md.h"
|
||||
|
||||
const PebbleProcessMd* weather_app_get_info();
|
556
src/fw/apps/system_apps/weather/weather_app_layout.c
Normal file
556
src/fw/apps/system_apps/weather/weather_app_layout.c
Normal file
|
@ -0,0 +1,556 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "weather_app_layout.h"
|
||||
|
||||
#include "applib/fonts/fonts.h"
|
||||
#include "applib/graphics/gcontext.h"
|
||||
#include "applib/graphics/gdraw_command_image.h"
|
||||
#include "applib/graphics/gpath.h"
|
||||
#include "applib/graphics/graphics.h"
|
||||
#include "applib/graphics/graphics_circle.h"
|
||||
#include "applib/graphics/gtypes.h"
|
||||
#include "applib/graphics/text.h"
|
||||
#include "applib/ui/animation.h"
|
||||
#include "applib/ui/animation_interpolate.h"
|
||||
#include "applib/ui/animation_timing.h"
|
||||
#include "applib/ui/content_indicator.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/kino/kino_reel/morph_square.h"
|
||||
#include "applib/ui/kino/kino_reel/transform.h"
|
||||
#include "applib/ui/window.h"
|
||||
#include "apps/system_apps/timeline/text_node.h"
|
||||
#include "font_resource_keys.auto.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
#include "services/common/i18n/i18n.h"
|
||||
#include "services/normal/timeline/timeline_resources.h"
|
||||
#include "services/normal/weather/weather_service.h"
|
||||
#include "services/normal/weather/weather_types.h"
|
||||
#include "system/logging.h"
|
||||
#include "system/passert.h"
|
||||
#include "util/size.h"
|
||||
#include "util/trig.h"
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#define WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT (18)
|
||||
#define WEATHER_APP_LAYOUT_TOP_PADDING PBL_IF_RECT_ELSE(0, 15)
|
||||
#define WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE (TimelineResourceSizeTiny)
|
||||
#define WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET PBL_IF_RECT_ELSE(3, 23)
|
||||
|
||||
static int prv_draw_text(GPoint offset, int max_width, GContext *context,
|
||||
const char *text, const GFont font,
|
||||
GColor font_color, GTextAlignment alignment) {
|
||||
const int height = fonts_get_font_height(font);
|
||||
const GRect box = (GRect) {offset, GSize(max_width, height)};
|
||||
|
||||
graphics_context_set_text_color(context, font_color);
|
||||
graphics_draw_text(context, text, font, box, GTextOverflowModeFill, alignment, NULL);
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
static void prv_draw_weather_background(const GRect *circle_bounding_box, GContext *context,
|
||||
GColor background_color) {
|
||||
if (!gcolor_is_invisible(background_color)) {
|
||||
graphics_context_set_fill_color(context, background_color);
|
||||
graphics_fill_oval(context, *circle_bounding_box, GOvalScaleModeFitCircle);
|
||||
}
|
||||
}
|
||||
|
||||
static void prv_fill_high_low_temp_buffer(const int high, const int low, char *buffer,
|
||||
const size_t buffer_size, const void *i18n_owner) {
|
||||
if ((high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) &&
|
||||
(low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP)) {
|
||||
/// Shown when neither high nor low temperature is known
|
||||
const char *both_temps_no_data = i18n_get("--° / --°", i18n_owner);
|
||||
strncpy(buffer, both_temps_no_data, strlen(both_temps_no_data));
|
||||
buffer[buffer_size - 1] = '\0';
|
||||
} else if (low == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
||||
/// Shown when only the day's high temperature is known, (e.g. "68° / --°")
|
||||
snprintf(buffer, buffer_size, i18n_get("%i° / --°", i18n_owner), high);
|
||||
} else if (high == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
||||
/// Shown when only the day's low temperature is known (e.g. "--° / 52°")
|
||||
snprintf(buffer, buffer_size, i18n_get("--° / %i°", i18n_owner), low);
|
||||
} else {
|
||||
/// A day's high and low temperature, separated by a foward slash (e.g. "68° / 52°")
|
||||
snprintf(buffer, buffer_size, i18n_get("%i° / %i°", i18n_owner), high, low);
|
||||
}
|
||||
}
|
||||
|
||||
#define GPS_ARROW_WIDTH (12)
|
||||
#define GPS_ARROW_HEIGHT (14)
|
||||
|
||||
static const GPoint s_gps_arrow_path_points[] = {
|
||||
{0, GPS_ARROW_HEIGHT},
|
||||
{(GPS_ARROW_WIDTH / 2), 0},
|
||||
{GPS_ARROW_WIDTH, GPS_ARROW_HEIGHT},
|
||||
// This 6/7 height ratio for the arrow notch achieves the design spec
|
||||
{(GPS_ARROW_WIDTH / 2), (GPS_ARROW_HEIGHT * 6 / 7)}
|
||||
};
|
||||
|
||||
static void prv_draw_gps_arrow_node_callback(GContext *ctx, const GRect *rect,
|
||||
UNUSED const GTextNodeDrawConfig *config, bool render,
|
||||
GSize *size_out, UNUSED void *user_data) {
|
||||
GPath gps_arrow_path = (GPath) {
|
||||
.num_points = ARRAY_LENGTH(s_gps_arrow_path_points),
|
||||
.points = (GPoint *)s_gps_arrow_path_points,
|
||||
.offset = rect->origin,
|
||||
// Ideal rotation would be 45 degrees, but the shape of the arrow matches the design best at
|
||||
// 38 degrees
|
||||
.rotation = DEG_TO_TRIGANGLE(38),
|
||||
};
|
||||
|
||||
if (render) {
|
||||
graphics_context_set_fill_color(ctx, GColorBlack);
|
||||
gpath_draw_filled(ctx, &gps_arrow_path);
|
||||
}
|
||||
if (size_out) {
|
||||
// Note that gpath_outer_rect() doesn't take into account the rotation; we'll add margin to the
|
||||
// location text node to account for it
|
||||
*size_out = gpath_outer_rect(&gps_arrow_path).size;
|
||||
}
|
||||
}
|
||||
|
||||
static GTextNode *prv_create_location_name_area_node(const WeatherLocationForecast *forecast,
|
||||
GFont location_font) {
|
||||
const GTextAlignment location_name_alignment = PBL_IF_RECT_ELSE(GTextAlignmentLeft,
|
||||
GTextAlignmentCenter);
|
||||
|
||||
// One node for the location name text and one node for the possible GPS arrow
|
||||
const size_t max_nodes = 2;
|
||||
GTextNodeHorizontal *horizontal_node = graphics_text_node_create_horizontal(max_nodes);
|
||||
horizontal_node->horizontal_alignment = location_name_alignment;
|
||||
|
||||
GTextNodeText *location_text_node = graphics_text_node_create_text(0);
|
||||
location_text_node->text = forecast->location_name;
|
||||
location_text_node->font = location_font;
|
||||
location_text_node->color = GColorBlack;
|
||||
location_text_node->overflow = GTextOverflowModeTrailingEllipsis;
|
||||
if (forecast->is_current_location) {
|
||||
// Horizontal spacing between location name and GPS arrow is spec'd by design to be 11 pixels
|
||||
location_text_node->node.margin = GSize(11, 0);
|
||||
}
|
||||
graphics_text_node_container_add_child(&horizontal_node->container, &location_text_node->node);
|
||||
|
||||
if (forecast->is_current_location) {
|
||||
GTextNodeCustom *arrow_node = graphics_text_node_create_custom(prv_draw_gps_arrow_node_callback,
|
||||
NULL);
|
||||
arrow_node->node.offset =
|
||||
GPoint(0, (int16_t)(fonts_get_font_cap_offset(location_font) / 2));
|
||||
graphics_text_node_container_add_child(&horizontal_node->container, &arrow_node->node);
|
||||
}
|
||||
|
||||
return &horizontal_node->container.node;
|
||||
}
|
||||
|
||||
static GSize prv_draw_location_name_area(GPoint offset, int max_width, GContext *ctx,
|
||||
GFont location_font,
|
||||
const WeatherLocationForecast *forecast) {
|
||||
GTextNode *location_name_area_node = prv_create_location_name_area_node(forecast, location_font);
|
||||
|
||||
GRect location_name_area_rect = (GRect) {
|
||||
.origin = offset,
|
||||
.size = GSize(max_width, fonts_get_font_height(location_font)),
|
||||
};
|
||||
|
||||
#if PBL_ROUND
|
||||
// On round the location name text and arrow can be obscured by the edges of the bezel, so we
|
||||
// horizontally inset the rectangle by a few pixels
|
||||
const int16_t horizontal_inset = 5;
|
||||
location_name_area_rect = grect_inset(location_name_area_rect,
|
||||
GEdgeInsets(0, horizontal_inset, 0));
|
||||
#endif
|
||||
|
||||
GSize location_name_area_size;
|
||||
graphics_text_node_draw(location_name_area_node, ctx, &location_name_area_rect, NULL,
|
||||
&location_name_area_size);
|
||||
graphics_text_node_destroy(location_name_area_node);
|
||||
return location_name_area_size;
|
||||
}
|
||||
// All text before the separator
|
||||
static void prv_draw_top_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
|
||||
int content_width,
|
||||
GContext *context) {
|
||||
const WeatherLocationForecast *forecast = layout->forecast;
|
||||
|
||||
current_offset->y += prv_draw_location_name_area(*current_offset, content_width, context,
|
||||
layout->location_font, forecast).h;
|
||||
|
||||
const int location_and_today_temperature_vertical_spacing = 7;
|
||||
current_offset->y += location_and_today_temperature_vertical_spacing;
|
||||
|
||||
char text_buffer[15] = {0};
|
||||
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
|
||||
|
||||
if (forecast->current_temp == WEATHER_SERVICE_LOCATION_FORECAST_UNKNOWN_TEMP) {
|
||||
/// Shown when today's current temperature is unknown
|
||||
const char *unknown_temp_string = i18n_get("--°", layout);
|
||||
strncpy(text_buffer, unknown_temp_string, strlen(unknown_temp_string));
|
||||
text_buffer[max_text_buff_size - 1] = '\0';
|
||||
} else {
|
||||
/// Today's current temperature (e.g. "68°")
|
||||
snprintf(text_buffer, max_text_buff_size, i18n_get("%i°", layout), forecast->current_temp);
|
||||
}
|
||||
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
|
||||
layout->temperature_font, GColorBlack, GTextAlignmentLeft);
|
||||
|
||||
prv_fill_high_low_temp_buffer(forecast->today_high, forecast->today_low, text_buffer,
|
||||
max_text_buff_size, layout);
|
||||
current_offset->y += prv_draw_text(*current_offset, content_width, context, text_buffer,
|
||||
layout->high_low_phrase_font, GColorBlack,
|
||||
GTextAlignmentLeft);
|
||||
const int today_high_low_gap_vertical_spacing_reduction = 2;
|
||||
current_offset->y -= today_high_low_gap_vertical_spacing_reduction;
|
||||
|
||||
current_offset->y += prv_draw_text(*current_offset, content_width, context,
|
||||
forecast->current_weather_phrase, layout->high_low_phrase_font,
|
||||
GColorBlack, GTextAlignmentLeft);
|
||||
}
|
||||
|
||||
// All text after the separator
|
||||
static void prv_draw_bottom_half_text(const WeatherAppLayout *layout, GPoint *current_offset,
|
||||
int content_width, GContext *context) {
|
||||
const WeatherLocationForecast *forecast = layout->forecast;
|
||||
|
||||
const int separator_tomorrow_title_vertical_spacing = 6;
|
||||
current_offset->y += separator_tomorrow_title_vertical_spacing;
|
||||
current_offset->y += prv_draw_text(*current_offset, content_width, context,
|
||||
/// Refers to the weather conditions for tomorrow
|
||||
i18n_get("TOMORROW", layout), layout->tomorrow_font,
|
||||
GColorBlack, GTextAlignmentLeft);
|
||||
char text_buffer[15] = {0};
|
||||
const size_t max_text_buff_size = ARRAY_LENGTH(text_buffer);
|
||||
prv_fill_high_low_temp_buffer(forecast->tomorrow_high, forecast->tomorrow_low, text_buffer,
|
||||
max_text_buff_size, layout);
|
||||
prv_draw_text(*current_offset, content_width, context, text_buffer, layout->high_low_phrase_font,
|
||||
GColorBlack, GTextAlignmentLeft);
|
||||
}
|
||||
|
||||
static void prv_draw_weather_icon_backgrounds(const WeatherAppLayout *layout,
|
||||
const GRect *content_bounds, GContext *context) {
|
||||
const WeatherLocationForecast *forecast = layout->forecast;
|
||||
// assume that both current and tomorrow weather icons are the same size
|
||||
const GSize icon_size = layout->current_weather_icon_layer.layer.bounds.size;
|
||||
const unsigned int weather_icon_bg_circle_diam = integer_sqrt(2 * icon_size.w * icon_size.h);
|
||||
|
||||
const int today_weather_bg_circle_top_margin = 28;
|
||||
GRect bg_circle_bounding_box = GRect(grect_get_max_x(content_bounds) -
|
||||
weather_icon_bg_circle_diam,
|
||||
today_weather_bg_circle_top_margin,
|
||||
weather_icon_bg_circle_diam, weather_icon_bg_circle_diam);
|
||||
|
||||
prv_draw_weather_background(&bg_circle_bounding_box, context,
|
||||
weather_type_get_bg_color(forecast->current_weather_type));
|
||||
|
||||
const int weather_bg_circle_vertical_spacing = 40;
|
||||
bg_circle_bounding_box.origin.y += weather_icon_bg_circle_diam +
|
||||
weather_bg_circle_vertical_spacing;
|
||||
prv_draw_weather_background(&bg_circle_bounding_box, context,
|
||||
weather_type_get_bg_color(forecast->tomorrow_weather_type));
|
||||
}
|
||||
|
||||
|
||||
static void prv_render_layout(Layer *layer, GContext *context) {
|
||||
// "Content" refers to everything except the dot separator
|
||||
const GRect content_bounds =
|
||||
grect_inset(layer->bounds, GEdgeInsets(0, WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET,
|
||||
0));
|
||||
const int content_x_offset = content_bounds.origin.x;
|
||||
const int content_width = content_bounds.size.w;
|
||||
|
||||
const WeatherAppLayout *layout = window_get_user_data(layer_get_window(layer));
|
||||
const WeatherLocationForecast *forecast = layout->forecast;
|
||||
|
||||
if (!forecast) {
|
||||
// Nothing to draw.
|
||||
return;
|
||||
}
|
||||
// start at 1 from the top to match design docs
|
||||
GPoint current_offset = GPoint(content_x_offset, 1);
|
||||
GPoint *offset = ¤t_offset;
|
||||
|
||||
prv_draw_top_half_text(layout, offset, content_width, context);
|
||||
|
||||
// dotted separator
|
||||
const int phrase_separator_vertical_spacing = 10;
|
||||
current_offset.y += phrase_separator_vertical_spacing;
|
||||
|
||||
const GPoint separator_start = GPoint(0, current_offset.y);
|
||||
graphics_context_set_stroke_width(context, 5);
|
||||
graphics_context_set_stroke_color(context, PBL_IF_COLOR_ELSE(GColorLightGray, GColorBlack));
|
||||
graphics_draw_horizontal_line_dotted(context, separator_start,
|
||||
layer->bounds.size.w);
|
||||
|
||||
if (!layout->animation_state.hide_bottom_half_text) {
|
||||
prv_draw_bottom_half_text(layout, offset, content_width, context);
|
||||
}
|
||||
prv_draw_weather_icon_backgrounds(layout, &content_bounds, context);
|
||||
}
|
||||
|
||||
static void prv_content_indicator_setup_direction(ContentIndicator *content_indicator,
|
||||
Layer *indicator_layer,
|
||||
ContentIndicatorDirection direction) {
|
||||
content_indicator_configure_direction(content_indicator, direction, &(ContentIndicatorConfig) {
|
||||
.layer = indicator_layer,
|
||||
.colors.foreground = GColorBlack,
|
||||
.colors.background = GColorLightGray,
|
||||
});
|
||||
}
|
||||
|
||||
void weather_app_layout_init(WeatherAppLayout *layout, const GRect *frame) {
|
||||
layout->location_font = fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD);
|
||||
layout->temperature_font = fonts_get_system_font(FONT_KEY_LECO_26_BOLD_NUMBERS_AM_PM);
|
||||
layout->high_low_phrase_font = fonts_get_system_font(FONT_KEY_GOTHIC_18);
|
||||
layout->tomorrow_font = fonts_get_system_font(FONT_KEY_GOTHIC_14_BOLD);
|
||||
|
||||
Layer *root_layer = &layout->root_layer;
|
||||
layer_init(root_layer, frame);
|
||||
|
||||
Layer *down_arrow_layer = &layout->down_arrow_layer;
|
||||
const GRect down_arrow_layer_frame = grect_inset(
|
||||
*frame, GEdgeInsets(frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
|
||||
layer_init(down_arrow_layer, &down_arrow_layer_frame);
|
||||
layer_add_child(root_layer, down_arrow_layer);
|
||||
|
||||
const int content_layer_side_padding = PBL_IF_RECT_ELSE(5, 12);
|
||||
const GRect content_layer_frame = grect_inset(*frame,
|
||||
GEdgeInsets(WEATHER_APP_LAYOUT_TOP_PADDING,
|
||||
content_layer_side_padding,
|
||||
WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT));
|
||||
|
||||
Layer *content_layer = &layout->content_layer;
|
||||
layer_init(content_layer, &content_layer_frame);
|
||||
layer_set_update_proc(content_layer, prv_render_layout);
|
||||
layer_add_child(root_layer, content_layer);
|
||||
|
||||
ContentIndicator *content_indicator = &layout->content_indicator;
|
||||
content_indicator_init(content_indicator);
|
||||
|
||||
prv_content_indicator_setup_direction(content_indicator, down_arrow_layer,
|
||||
ContentIndicatorDirectionDown);
|
||||
|
||||
const GSize icon_size =
|
||||
timeline_resources_get_gsize(WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE);
|
||||
|
||||
const int icon_layer_margin_top = PBL_IF_RECT_ELSE(33, 18);
|
||||
const int icon_layer_right_margin = 5;
|
||||
GRect icon_layer_frame = (GRect) {
|
||||
{
|
||||
content_layer_frame.size.w - icon_size.w - WEATHER_APP_LAYOUT_CONTENT_LAYER_HORIZONTAL_INSET -
|
||||
icon_layer_right_margin,
|
||||
content_layer_frame.origin.y + icon_layer_margin_top
|
||||
},
|
||||
icon_size
|
||||
};
|
||||
|
||||
KinoLayer *current_weather_icon_layer = &layout->current_weather_icon_layer;
|
||||
kino_layer_init(current_weather_icon_layer, &icon_layer_frame);
|
||||
layer_add_child(content_layer, kino_layer_get_layer(current_weather_icon_layer));
|
||||
|
||||
const int icon_layer_spacing = 50;
|
||||
icon_layer_frame.origin.y += icon_size.h + icon_layer_spacing;
|
||||
|
||||
KinoLayer *tomorrow_weather_icon_layer = &layout->tomorrow_weather_icon_layer;
|
||||
kino_layer_init(tomorrow_weather_icon_layer, &icon_layer_frame);
|
||||
layer_add_child(content_layer, kino_layer_get_layer(tomorrow_weather_icon_layer));
|
||||
}
|
||||
|
||||
static uint32_t prv_get_resource_id_for_weather_type(WeatherType type) {
|
||||
const TimelineResourceInfo timeline_res = {
|
||||
.res_id = weather_type_get_timeline_resource_id(type),
|
||||
};
|
||||
AppResourceInfo icon_res_info;
|
||||
timeline_resources_get_id(&timeline_res, WEATHER_APP_LAYOUT_TIMELINE_ICON_RESOURCE_SIZE,
|
||||
&icon_res_info);
|
||||
return icon_res_info.res_id;
|
||||
}
|
||||
|
||||
void weather_app_layout_set_data(WeatherAppLayout *layout,
|
||||
const WeatherLocationForecast *forecast) {
|
||||
layout->forecast = forecast;
|
||||
|
||||
const uint32_t current_weather_res_id = forecast ?
|
||||
prv_get_resource_id_for_weather_type(forecast->current_weather_type) : RESOURCE_ID_INVALID;
|
||||
const uint32_t tomorrow_weather_res_id = forecast ?
|
||||
prv_get_resource_id_for_weather_type(forecast->tomorrow_weather_type) : RESOURCE_ID_INVALID;
|
||||
|
||||
kino_layer_set_reel_with_resource(&layout->current_weather_icon_layer, current_weather_res_id);
|
||||
kino_layer_set_reel_with_resource(&layout->tomorrow_weather_icon_layer, tomorrow_weather_res_id);
|
||||
|
||||
layer_mark_dirty(&layout->root_layer);
|
||||
}
|
||||
|
||||
void weather_app_layout_set_down_arrow_visible(WeatherAppLayout *layout, bool is_down_visible) {
|
||||
content_indicator_set_content_available(&layout->content_indicator, ContentIndicatorDirectionDown,
|
||||
is_down_visible);
|
||||
}
|
||||
|
||||
void weather_app_layout_deinit(WeatherAppLayout *layout) {
|
||||
i18n_free_all(layout);
|
||||
layer_deinit(&layout->root_layer);
|
||||
}
|
||||
|
||||
// Down arrow layer grows until a point, after which the entire content teleports to a height
|
||||
// slightly higher than its resting position, then relaxes into place
|
||||
static void prv_down_animation_update(Animation *animation, AnimationProgress normalized) {
|
||||
WeatherAppLayout *layout = animation_get_context(animation);
|
||||
// Progress at which to switch from the down arrow growing to entire content relaxing downwards
|
||||
const AnimationProgress animation_cut_frame_progress =
|
||||
(interpolate_moook_in_duration() * ANIMATION_NORMALIZED_MAX) / interpolate_moook_duration();
|
||||
// Progress at which to hide "TOMORROW" and tomorrow high / low temperature text
|
||||
const AnimationProgress animation_hide_bottom_half_text_progress =
|
||||
(animation_cut_frame_progress * 2) / 3;
|
||||
|
||||
int down_arrow_layer_height = WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT;
|
||||
layout->animation_state.hide_bottom_half_text = false;
|
||||
|
||||
if (normalized <= animation_cut_frame_progress) {
|
||||
if (normalized >= animation_hide_bottom_half_text_progress) {
|
||||
layout->animation_state.hide_bottom_half_text = true;
|
||||
}
|
||||
// renormalize the progress so that interpolate_moook_in_only works as expected
|
||||
int32_t new_normalized = animation_timing_scaled(normalized, ANIMATION_NORMALIZED_MIN,
|
||||
animation_cut_frame_progress);
|
||||
const int additional_down_arrow_height = 25;
|
||||
// grow the down arrow layer
|
||||
down_arrow_layer_height += interpolate_moook_in_only(new_normalized, 0,
|
||||
additional_down_arrow_height);
|
||||
} else {
|
||||
// We've cut, so display the next forecast's data
|
||||
if (layout->animation_state.next_forecast) {
|
||||
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
|
||||
layout->animation_state.next_forecast = NULL;
|
||||
}
|
||||
int32_t new_normalized = animation_timing_scaled(normalized, animation_cut_frame_progress,
|
||||
ANIMATION_NORMALIZED_MAX);
|
||||
|
||||
// Relax the content by changing its top margin
|
||||
const int animation_margin_top_from = WEATHER_APP_LAYOUT_TOP_PADDING - PBL_IF_RECT_ELSE(10, 15);
|
||||
const int animation_margin_top_to = WEATHER_APP_LAYOUT_TOP_PADDING;
|
||||
const int num_frames_from = 1;
|
||||
const bool bounce_back = false;
|
||||
int animation_margin_top = interpolate_moook_out(new_normalized, animation_margin_top_from,
|
||||
animation_margin_top_to, num_frames_from,
|
||||
bounce_back);
|
||||
layout->content_layer.frame.origin.y = animation_margin_top;
|
||||
|
||||
// The down arrow's height follows the content margin. It starts off large, then goes back to
|
||||
// its original size, as the content relaxes into place
|
||||
down_arrow_layer_height += (-animation_margin_top + WEATHER_APP_LAYOUT_TOP_PADDING);
|
||||
}
|
||||
|
||||
const GRect down_arrow_layer_frame = grect_inset(layout->root_layer.frame,
|
||||
GEdgeInsets(layout->root_layer.frame.size.h - down_arrow_layer_height, 0, 0));
|
||||
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
|
||||
|
||||
layer_mark_dirty(&layout->root_layer);
|
||||
}
|
||||
|
||||
// moves the entire root layer up back into place
|
||||
static void prv_up_animation_update(Animation *animation, AnimationProgress normalized) {
|
||||
const int root_layer_top_margin_from = (WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT * 2) / 3;
|
||||
const int root_layer_top_margin_to = 0;
|
||||
const int num_frames_from = 1;
|
||||
const bool bounce_back = false;
|
||||
int root_layer_top_margin = interpolate_moook_out(normalized, root_layer_top_margin_from,
|
||||
root_layer_top_margin_to, num_frames_from, bounce_back);
|
||||
|
||||
WeatherAppLayout *layout = animation_get_context(animation);
|
||||
if (layout->animation_state.next_forecast) {
|
||||
layout->forecast = layout->animation_state.next_forecast;
|
||||
layout->animation_state.next_forecast = NULL;
|
||||
}
|
||||
|
||||
layout->root_layer.frame.origin.y = root_layer_top_margin;
|
||||
layer_set_frame(&layout->root_layer, &layout->root_layer.frame);
|
||||
}
|
||||
|
||||
static void prv_animation_stopped(Animation *animation, bool finished, void *context) {
|
||||
WeatherAppLayout *layout = context;
|
||||
if (layout->animation_state.next_forecast) {
|
||||
weather_app_layout_set_data(layout, layout->animation_state.next_forecast);
|
||||
layout->animation_state.next_forecast = NULL;
|
||||
}
|
||||
layout->animation_state.hide_bottom_half_text = false;
|
||||
|
||||
GRect *root_layer_frame = &layout->root_layer.frame;
|
||||
root_layer_frame->origin.y = 0;
|
||||
layer_set_frame(&layout->root_layer, root_layer_frame);
|
||||
|
||||
GRect *content_layer_frame = &layout->content_layer.frame;
|
||||
content_layer_frame->origin.y = WEATHER_APP_LAYOUT_TOP_PADDING;
|
||||
layer_set_frame(&layout->content_layer, content_layer_frame);
|
||||
|
||||
const GRect down_arrow_layer_frame = grect_inset(*root_layer_frame,
|
||||
GEdgeInsets(root_layer_frame->size.h - WEATHER_APP_LAYOUT_ARROW_LAYER_HEIGHT, 0, 0));
|
||||
layer_set_frame(&layout->down_arrow_layer, &down_arrow_layer_frame);
|
||||
}
|
||||
|
||||
static const AnimationImplementation s_down_animation_implementation = {
|
||||
.update = &prv_down_animation_update,
|
||||
};
|
||||
|
||||
static const AnimationImplementation s_up_animation_implementation = {
|
||||
.update = &prv_up_animation_update,
|
||||
};
|
||||
|
||||
static const AnimationHandlers s_animation_handlers = {
|
||||
.stopped = &prv_animation_stopped,
|
||||
};
|
||||
|
||||
static void prv_morph_weather_icons(KinoLayer *icon_layer, WeatherType from, WeatherType to,
|
||||
uint32_t duration) {
|
||||
uint32_t from_image_res_id =
|
||||
prv_get_resource_id_for_weather_type(from);
|
||||
KinoReel *from_reel = kino_reel_create_with_resource(from_image_res_id);
|
||||
KinoReel *to_reel = kino_reel_create_with_resource(prv_get_resource_id_for_weather_type(to));
|
||||
|
||||
KinoReel *icon_reel = kino_reel_morph_square_create(from_reel, true);
|
||||
kino_reel_transform_set_to_reel(icon_reel, to_reel, true);
|
||||
kino_reel_transform_set_transform_duration(icon_reel, duration);
|
||||
|
||||
kino_layer_set_reel(icon_layer, icon_reel, true);
|
||||
kino_layer_play(icon_layer);
|
||||
}
|
||||
|
||||
void weather_app_layout_animate(WeatherAppLayout *layout, WeatherLocationForecast *new_forecast,
|
||||
bool animate_down) {
|
||||
animation_unschedule_all();
|
||||
|
||||
const uint32_t anim_duration = animate_down ? interpolate_moook_duration() :
|
||||
interpolate_moook_out_duration();
|
||||
layout->animation_state.next_forecast = new_forecast;
|
||||
Animation *animation = animation_create();
|
||||
animation_set_duration(animation, anim_duration);
|
||||
InterpolateInt64Function interpolation = animate_down ? interpolate_moook :
|
||||
interpolate_moook_in_only;
|
||||
animation_set_custom_interpolation(animation, interpolation);
|
||||
animation_set_handlers(animation, s_animation_handlers, layout);
|
||||
const AnimationImplementation *implementation = animate_down ? &s_down_animation_implementation :
|
||||
&s_up_animation_implementation;
|
||||
animation_set_implementation(animation, implementation);
|
||||
animation_schedule(animation);
|
||||
|
||||
prv_morph_weather_icons(&layout->current_weather_icon_layer,
|
||||
layout->forecast->current_weather_type,
|
||||
new_forecast->current_weather_type, anim_duration);
|
||||
|
||||
prv_morph_weather_icons(&layout->tomorrow_weather_icon_layer,
|
||||
layout->forecast->tomorrow_weather_type,
|
||||
new_forecast->tomorrow_weather_type, anim_duration);
|
||||
}
|
52
src/fw/apps/system_apps/weather/weather_app_layout.h
Normal file
52
src/fw/apps/system_apps/weather/weather_app_layout.h
Normal file
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/graphics/gdraw_command_image.h"
|
||||
#include "applib/ui/content_indicator_private.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "services/normal/weather/weather_service.h"
|
||||
|
||||
typedef struct WeatherAppLayout {
|
||||
Layer root_layer;
|
||||
Layer content_layer;
|
||||
KinoLayer current_weather_icon_layer;
|
||||
KinoLayer tomorrow_weather_icon_layer;
|
||||
const WeatherLocationForecast *forecast;
|
||||
GFont location_font;
|
||||
GFont temperature_font;
|
||||
GFont high_low_phrase_font;
|
||||
GFont tomorrow_font;
|
||||
Layer down_arrow_layer;
|
||||
ContentIndicator content_indicator;
|
||||
struct { // used during animations
|
||||
const WeatherLocationForecast *next_forecast;
|
||||
bool hide_bottom_half_text;
|
||||
} animation_state;
|
||||
} WeatherAppLayout;
|
||||
|
||||
void weather_app_layout_init(WeatherAppLayout *layout, const GRect *frame);
|
||||
|
||||
void weather_app_layout_set_data(WeatherAppLayout *layout,
|
||||
const WeatherLocationForecast *forecast);
|
||||
|
||||
void weather_app_layout_set_down_arrow_visible(WeatherAppLayout *layout, bool is_down_visible);
|
||||
|
||||
void weather_app_layout_deinit(WeatherAppLayout *layout);
|
||||
|
||||
void weather_app_layout_animate(WeatherAppLayout *layout, WeatherLocationForecast *new_forecast,
|
||||
bool animate_down);
|
85
src/fw/apps/system_apps/weather/weather_app_splash_screen.c
Normal file
85
src/fw/apps/system_apps/weather/weather_app_splash_screen.c
Normal file
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "weather_app_splash_screen.h"
|
||||
|
||||
#include "applib/app.h"
|
||||
#include "applib/ui/kino/kino_layer.h"
|
||||
#include "applib/ui/ui.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
#include "resource/resource_ids.auto.h"
|
||||
|
||||
typedef struct SplashScreenData {
|
||||
Window window;
|
||||
KinoLayer logo_layer;
|
||||
AppTimer *timer;
|
||||
uint32_t timeout_ms;
|
||||
} SplashScreenData;
|
||||
|
||||
static void prv_splash_screen_finished_callback(void *cb_data) {
|
||||
SplashScreenData *data = cb_data;
|
||||
data->timer = NULL;
|
||||
const bool animated = false;
|
||||
app_window_stack_remove(&data->window, animated);
|
||||
}
|
||||
|
||||
static void prv_window_unload(Window *window) {
|
||||
SplashScreenData *data = window_get_user_data(window);
|
||||
kino_layer_deinit(&data->logo_layer);
|
||||
// Execute conditional only if the user presses back while the splash screen is showing
|
||||
if (data->timer) {
|
||||
app_timer_cancel(data->timer);
|
||||
const bool animated = true;
|
||||
app_window_stack_pop_all(animated);
|
||||
}
|
||||
app_free(data);
|
||||
}
|
||||
|
||||
static void prv_window_load(Window *window) {
|
||||
SplashScreenData *data = window_get_user_data(window);
|
||||
Layer *window_root_layer = &window->layer;
|
||||
KinoLayer *logo_layer = &data->logo_layer;
|
||||
kino_layer_init(logo_layer, &window_root_layer->bounds);
|
||||
kino_layer_set_reel_with_resource(logo_layer,
|
||||
RESOURCE_ID_WEATHER_CHANNEL_LOGO);
|
||||
layer_add_child(window_root_layer,
|
||||
kino_layer_get_layer(logo_layer));
|
||||
|
||||
data->timer = app_timer_register(data->timeout_ms,
|
||||
prv_splash_screen_finished_callback,
|
||||
data);
|
||||
}
|
||||
|
||||
void weather_app_splash_screen_push(uint32_t timeout_ms) {
|
||||
SplashScreenData *data = app_zalloc_check(sizeof(SplashScreenData));
|
||||
data->timeout_ms = timeout_ms;
|
||||
|
||||
Window *window = &data->window;
|
||||
window_init(window, WINDOW_NAME("Weather - Splash Screen"));
|
||||
|
||||
const GColor background_color = PBL_IF_COLOR_ELSE(GColorBlue, GColorBlack);
|
||||
window_set_background_color(window, background_color);
|
||||
|
||||
const WindowHandlers window_handlers = {
|
||||
.load = prv_window_load,
|
||||
.unload = prv_window_unload,
|
||||
};
|
||||
window_set_window_handlers(window, &window_handlers);
|
||||
window_set_user_data(window, data);
|
||||
|
||||
const bool animated = false;
|
||||
app_window_stack_push(window, animated);
|
||||
}
|
21
src/fw/apps/system_apps/weather/weather_app_splash_screen.h
Normal file
21
src/fw/apps/system_apps/weather/weather_app_splash_screen.h
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/ui.h"
|
||||
|
||||
void weather_app_splash_screen_push(uint32_t timeout_ms);
|
63
src/fw/apps/system_apps/weather/weather_app_warning_dialog.c
Normal file
63
src/fw/apps/system_apps/weather/weather_app_warning_dialog.c
Normal file
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "weather_app_warning_dialog.h"
|
||||
|
||||
#include "applib/ui/app_window_stack.h"
|
||||
#include "applib/ui/dialogs/dialog.h"
|
||||
#include "applib/ui/dialogs/expandable_dialog.h"
|
||||
#include "kernel/pbl_malloc.h"
|
||||
|
||||
typedef struct WeatherAppWarningDialogData {
|
||||
WeatherAppWarningDialogDismissedCallback dismissed_cb;
|
||||
} WeatherAppWarningDialogData;
|
||||
|
||||
static void prv_warning_dialog_unload(void *context) {
|
||||
WeatherAppWarningDialogData *data = context;
|
||||
if (data->dismissed_cb) {
|
||||
data->dismissed_cb();
|
||||
}
|
||||
task_free(data);
|
||||
}
|
||||
|
||||
static void prv_warning_dialog_select_handler(ClickRecognizerRef recognizer, void *context) {
|
||||
ExpandableDialog *expandable_dialog = context;
|
||||
expandable_dialog_pop(expandable_dialog);
|
||||
}
|
||||
|
||||
WeatherAppWarningDialog *weather_app_warning_dialog_push(const char *localized_string,
|
||||
WeatherAppWarningDialogDismissedCallback dismissed_cb) {
|
||||
WeatherAppWarningDialogData *data = task_zalloc_check(sizeof(WeatherAppWarningDialogData));
|
||||
ExpandableDialog *expandable_dialog = expandable_dialog_create("Weather - warning dialog");
|
||||
|
||||
Dialog *dialog = expandable_dialog_get_dialog(expandable_dialog);
|
||||
dialog_set_destroy_on_pop(dialog, false);
|
||||
dialog_set_icon(dialog, RESOURCE_ID_GENERIC_WARNING_TINY);
|
||||
dialog_set_text(dialog, localized_string);
|
||||
const DialogCallbacks callbacks = {
|
||||
.unload = prv_warning_dialog_unload,
|
||||
};
|
||||
dialog_set_callbacks(dialog, &callbacks, data);
|
||||
|
||||
expandable_dialog_show_action_bar(expandable_dialog, true);
|
||||
expandable_dialog_set_select_action(expandable_dialog, RESOURCE_ID_ACTION_BAR_ICON_CHECK,
|
||||
prv_warning_dialog_select_handler);
|
||||
|
||||
data->dismissed_cb = dismissed_cb;
|
||||
app_expandable_dialog_push(expandable_dialog);
|
||||
|
||||
return expandable_dialog;
|
||||
}
|
28
src/fw/apps/system_apps/weather/weather_app_warning_dialog.h
Normal file
28
src/fw/apps/system_apps/weather/weather_app_warning_dialog.h
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright 2024 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "applib/ui/dialogs/expandable_dialog.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef ExpandableDialog WeatherAppWarningDialog;
|
||||
|
||||
typedef void (*WeatherAppWarningDialogDismissedCallback)(void);
|
||||
|
||||
WeatherAppWarningDialog *weather_app_warning_dialog_push(const char *localized_string,
|
||||
WeatherAppWarningDialogDismissedCallback dismissed_cb);
|
Loading…
Add table
Add a link
Reference in a new issue