mirror of https://github.com/google/pebble
1280 lines
45 KiB
C
1280 lines
45 KiB
C
/*
|
|
* 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 "pin_window.h"
|
|
#include "timeline.h"
|
|
#include "timeline_animations.h"
|
|
#include "timeline_model.h"
|
|
|
|
#include "applib/app.h"
|
|
#include "applib/ui/animation_interpolate.h"
|
|
#include "applib/ui/animation_timing.h"
|
|
#include "applib/ui/app_window_stack.h"
|
|
#include "applib/ui/kino/kino_reel/scale_segmented.h"
|
|
#include "applib/ui/kino/kino_reel/unfold.h"
|
|
#include "applib/ui/ui.h"
|
|
#include "drivers/rtc.h"
|
|
#include "kernel/event_loop.h"
|
|
#include "kernel/pbl_malloc.h"
|
|
#include "process_management/app_manager.h"
|
|
#include "resource/resource_ids.auto.h"
|
|
#include "resource/timeline_resource_ids.auto.h"
|
|
#include "services/common/analytics/analytics.h"
|
|
#include "services/common/compositor/compositor_transitions.h"
|
|
#include "services/common/i18n/i18n.h"
|
|
#include "services/normal/blob_db/pin_db.h"
|
|
#include "services/normal/timeline/actions_endpoint.h"
|
|
#include "services/normal/timeline/attribute.h"
|
|
#include "shell/normal/watchface.h"
|
|
#include "syscall/syscall.h"
|
|
#include "system/logging.h"
|
|
#include "system/passert.h"
|
|
#include "util/array.h"
|
|
#include "util/attributes.h"
|
|
#include "util/size.h"
|
|
#include "util/uuid.h"
|
|
|
|
// This is used to determine whether this app was launched as Timeline or Timeline Past.
|
|
// See timeline_get_app_info, timeline_past_get_app_info, and the usage of sys_get_app_uuid.
|
|
// uuid: DAAE3686-BFF6-4BA5-921B-262F847BB6E8
|
|
#define TIMELINE_PAST_UUID_INIT {0xDA, 0xAE, 0x36, 0x86, 0xBF, 0xF6, 0x4B, 0xA5, \
|
|
0x92, 0x1B, 0x26, 0x2F, 0x84, 0x7B, 0xB6, 0xE8}
|
|
|
|
#if PBL_ROUND || PLATFORM_TINTIN
|
|
// Tintin looks funny with the dot animation, but it results in less code space usage
|
|
#define ANIMATION_DOT 1
|
|
#define ANIMATION_SLIDE 0
|
|
#else
|
|
#define ANIMATION_DOT 0
|
|
#define ANIMATION_SLIDE 1
|
|
#endif
|
|
|
|
typedef struct TimelineAppStyle {
|
|
int16_t peek_offset_y;
|
|
int16_t peek_icon_offset_y;
|
|
} TimelineAppStyle;
|
|
|
|
static const TimelineAppStyle s_style_medium = {
|
|
.peek_icon_offset_y = PEEK_LAYER_ICON_OFFSET_Y,
|
|
};
|
|
|
|
static const TimelineAppStyle s_style_large = {
|
|
.peek_offset_y = -7,
|
|
.peek_icon_offset_y = -16,
|
|
};
|
|
|
|
static const TimelineAppStyle * const s_styles[NumPreferredContentSizes] = {
|
|
[PreferredContentSizeSmall] = &s_style_medium,
|
|
[PreferredContentSizeMedium] = &s_style_medium,
|
|
[PreferredContentSizeLarge] = &s_style_large,
|
|
[PreferredContentSizeExtraLarge] = &s_style_large,
|
|
};
|
|
|
|
static TimelineAppData *s_app_data;
|
|
|
|
static const uint32_t TIMELINE_SLIDE_ANIMATION_MS = 150;
|
|
static const uint32_t PEEK_SLIDE_ANIMATION_MS = 300;
|
|
static const uint32_t PEEK_SHOW_TIME_MS = 660;
|
|
|
|
|
|
static const TimelineAppStyle *prv_get_style(void) {
|
|
return s_styles[PreferredContentSizeDefault];
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// State Machine
|
|
/////////////////////////////////////
|
|
|
|
static bool prv_can_transition_state(TimelineAppData *data, TimelineAppState next_state) {
|
|
// all non-exit states can transition to exit
|
|
if (data->state != TimelineAppStateExit &&
|
|
next_state == TimelineAppStateExit) {
|
|
return true;
|
|
}
|
|
switch (data->state) {
|
|
case TimelineAppStateNone:
|
|
return (next_state == TimelineAppStatePeek ||
|
|
next_state == TimelineAppStateHidePeek ||
|
|
next_state == TimelineAppStateFarDayHidePeek ||
|
|
next_state == TimelineAppStateNoEvents);
|
|
case TimelineAppStatePeek:
|
|
return (next_state == TimelineAppStateHidePeek);
|
|
case TimelineAppStateHidePeek:
|
|
return (next_state == TimelineAppStateStationary);
|
|
case TimelineAppStateFarDayHidePeek:
|
|
return (next_state == TimelineAppStateDaySeparator);
|
|
case TimelineAppStateStationary:
|
|
return (next_state == TimelineAppStateUpDown ||
|
|
next_state == TimelineAppStatePushCard ||
|
|
next_state == TimelineAppStateNoEvents ||
|
|
next_state == TimelineAppStateInactive);
|
|
case TimelineAppStateUpDown:
|
|
return (next_state == TimelineAppStateUpDown ||
|
|
next_state == TimelineAppStateShowDaySeparator ||
|
|
next_state == TimelineAppStateStationary);
|
|
case TimelineAppStateShowDaySeparator:
|
|
return (next_state == TimelineAppStateDaySeparator);
|
|
case TimelineAppStateDaySeparator:
|
|
return (next_state == TimelineAppStateHideDaySeparator);
|
|
case TimelineAppStateHideDaySeparator:
|
|
return (next_state == TimelineAppStateStationary);
|
|
case TimelineAppStatePushCard:
|
|
return (next_state == TimelineAppStateCard ||
|
|
next_state == TimelineAppStatePopCard);
|
|
case TimelineAppStateCard:
|
|
return (next_state == TimelineAppStatePopCard ||
|
|
next_state == TimelineAppStateStationary);
|
|
case TimelineAppStatePopCard:
|
|
return (next_state == TimelineAppStateStationary ||
|
|
next_state == TimelineAppStatePushCard);
|
|
case TimelineAppStateNoEvents:
|
|
return (next_state == TimelineAppStateInactive);
|
|
case TimelineAppStateInactive:
|
|
case TimelineAppStateExit:
|
|
return false;
|
|
default:
|
|
WTF;
|
|
}
|
|
}
|
|
|
|
static bool prv_set_state(TimelineAppData *data, TimelineAppState next_state) {
|
|
const bool can_transition = prv_can_transition_state(data, next_state);
|
|
PBL_LOG(LOG_LEVEL_DEBUG, "state transition %d->%d valid:%d",
|
|
data->state, next_state, can_transition);
|
|
if (can_transition) {
|
|
data->state = next_state;
|
|
}
|
|
return can_transition;
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Exit Animation & Inactivity Timer
|
|
/////////////////////////////////////
|
|
|
|
T_STATIC void prv_init_peek_layer(TimelineAppData *data);
|
|
|
|
static void prv_launch_watchface(void *data) {
|
|
#ifdef SHELL_SDK
|
|
// FIXME: We don't want to show off our unfinished animations in the sdkshell
|
|
watchface_launch_default(NULL);
|
|
#else
|
|
|
|
#if PLATFORM_TINTIN
|
|
const CompositorTransition *transition = NULL;
|
|
#else
|
|
const bool is_future = (s_app_data->timeline_model.direction == TimelineIterDirectionFuture);
|
|
const bool to_timeline = false;
|
|
const CompositorTransition *transition = PBL_IF_RECT_ELSE(
|
|
compositor_slide_transition_timeline_get(is_future, to_timeline, timeline_model_is_empty()),
|
|
compositor_dot_transition_timeline_get(is_future, to_timeline));
|
|
#endif
|
|
|
|
watchface_launch_default(transition);
|
|
#endif
|
|
}
|
|
|
|
static void prv_cleanup_timer(EventedTimerID *timer) {
|
|
if (evented_timer_exists(*timer)) {
|
|
evented_timer_cancel(*timer);
|
|
*timer = EVENTED_TIMER_INVALID_ID;
|
|
}
|
|
}
|
|
|
|
static void prv_exit_timer_callback(void *context) {
|
|
TimelineAppData *data = context;
|
|
data->timeline_layer.animating_intro_or_exit = false;
|
|
launcher_task_add_callback(prv_launch_watchface, data);
|
|
}
|
|
|
|
static void prv_intro_or_exit_anim_started(Animation *anim, void *context) {
|
|
TimelineAppData *data = context;
|
|
data->timeline_layer.animating_intro_or_exit = true;
|
|
}
|
|
|
|
static void prv_exit_anim_stopped(Animation *animation, bool finished, void *context) {
|
|
// we must use a timer to allow the last frame to render
|
|
const int exit_timeout_ms = 2 * ANIMATION_TARGET_FRAME_INTERVAL_MS;
|
|
evented_timer_register(exit_timeout_ms, false, prv_exit_timer_callback, context);
|
|
}
|
|
|
|
//! Used for setting the animation frame source and/or destination of the peek layer.
|
|
//! If use_pin is true, the animation frame size and position will be that of the first pin icon.
|
|
//! If shift_offscreen is true, the frame will be shifted by the screen row amount in a direction
|
|
//! depending on the scroll direction.
|
|
static void prv_get_icon_animation_frame(TimelineAppData *data, GRect *icon_frame_out,
|
|
bool use_pin, bool shift_offscreen) {
|
|
#if ANIMATION_DOT
|
|
const GRect *layer_frame = &data->timeline_window.layer.frame;
|
|
*icon_frame_out = (GRect) {
|
|
.origin.x = layer_frame->origin.x + (layer_frame->size.w - UNFOLD_DOT_SIZE_PX) / 2,
|
|
.origin.y = layer_frame->origin.y + (layer_frame->size.h - UNFOLD_DOT_SIZE_PX) / 2,
|
|
.size = UNFOLD_DOT_SIZE,
|
|
};
|
|
#elif ANIMATION_SLIDE
|
|
GRect icon_frame;
|
|
TimelineLayout *first_timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (first_timeline_layout && use_pin) {
|
|
GRect frame;
|
|
timeline_layer_get_layout_frame(&data->timeline_layer, TIMELINE_LAYER_FIRST_VISIBLE_LAYOUT,
|
|
&frame);
|
|
timeline_layout_get_icon_frame(&frame, data->timeline_layer.scroll_direction, &icon_frame);
|
|
} else {
|
|
// Since there is no pin, we need the peek size, which is the large size
|
|
icon_frame = (GRect) { .size = TIMELINE_LARGE_RESOURCE_SIZE };
|
|
grect_align(&icon_frame, &data->peek_layer.layer.frame, GAlignCenter, false);
|
|
const TimelineAppStyle *style = prv_get_style();
|
|
icon_frame.origin.y += style->peek_icon_offset_y;
|
|
}
|
|
if (shift_offscreen) {
|
|
if (data->timeline_model.direction == TimelineIterDirectionPast) {
|
|
icon_frame.origin.y -= DISP_ROWS;
|
|
} else {
|
|
icon_frame.origin.y += DISP_ROWS;
|
|
}
|
|
}
|
|
*icon_frame_out = icon_frame;
|
|
#endif
|
|
}
|
|
|
|
static Animation *prv_create_peek_exit_anim(TimelineAppData *data, TimelineAppState prev_state,
|
|
uint32_t duration) {
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
if (prev_state == TimelineAppStateNoEvents ||
|
|
prev_state == TimelineAppStatePeek ||
|
|
prev_state == TimelineAppStateHidePeek) {
|
|
prv_cleanup_timer(&data->intro_timer_id);
|
|
} else if (prev_state == TimelineAppStateStationary ||
|
|
prev_state == TimelineAppStateUpDown) {
|
|
TimelineLayout *first_timeline_layout =
|
|
timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (!first_timeline_layout) {
|
|
return NULL;
|
|
}
|
|
|
|
prv_init_peek_layer(data);
|
|
|
|
GRect icon_from;
|
|
layer_get_global_frame((Layer *)&first_timeline_layout->icon_layer, &icon_from);
|
|
|
|
peek_layer_set_icon_with_size(peek_layer, &first_timeline_layout->icon_info,
|
|
TimelineResourceSizeTiny, icon_from);
|
|
} else {
|
|
return NULL;
|
|
}
|
|
|
|
GRect icon_to;
|
|
const bool use_pin = true;
|
|
const bool shift_offscreen = true;
|
|
prv_get_icon_animation_frame(data, &icon_to, use_pin, shift_offscreen);
|
|
|
|
peek_layer_clear_fields(peek_layer);
|
|
peek_layer_set_scale_to(peek_layer, icon_to);
|
|
peek_layer_set_duration(peek_layer, duration);
|
|
|
|
#if PLATFORM_TINTIN
|
|
return (Animation *)peek_layer_create_play_animation(&data->peek_layer);
|
|
#else
|
|
// Play only a section to reduce the duration to the scaling, ignoring the PDCS duration
|
|
return (Animation *)peek_layer_create_play_section_animation(&data->peek_layer, 0, duration);
|
|
#endif
|
|
}
|
|
|
|
static Animation *prv_create_sidebar_animation(TimelineAppData *data, bool open) {
|
|
int16_t to_sidebar_width;
|
|
if (open) {
|
|
to_sidebar_width = timeline_layer_get_ideal_sidebar_width();
|
|
} else {
|
|
const GRect *layer_frame = &data->timeline_window.layer.frame;
|
|
to_sidebar_width = layer_frame->size.w;
|
|
#if PBL_ROUND
|
|
// Use a larger width to ensure we fill the entire screen since we use a circular background
|
|
to_sidebar_width += 25;
|
|
#endif
|
|
}
|
|
return timeline_layer_create_sidebar_animation(&data->timeline_layer, to_sidebar_width);
|
|
}
|
|
|
|
static void prv_exit(TimelineAppData *data) {
|
|
UNUSED const TimelineAppState prev_state = data->state;
|
|
if (!prv_set_state(data, TimelineAppStateExit)) {
|
|
return;
|
|
}
|
|
|
|
#if ANIMATION_SLIDE
|
|
prv_launch_watchface(data);
|
|
#elif ANIMATION_DOT
|
|
const uint32_t duration = interpolate_moook_in_duration();
|
|
|
|
animation_unschedule(data->current_animation);
|
|
layer_remove_child_layers((Layer *)&data->timeline_layer);
|
|
|
|
Animation *sidebar_slide = prv_create_sidebar_animation(data, false /* open */);
|
|
animation_set_duration(sidebar_slide, duration);
|
|
animation_set_handlers(sidebar_slide, (AnimationHandlers) {
|
|
.started = prv_intro_or_exit_anim_started,
|
|
.stopped = prv_exit_anim_stopped,
|
|
}, data);
|
|
|
|
Animation *peek_anim = prv_create_peek_exit_anim(data, prev_state, duration);
|
|
|
|
// Just play them at the same time
|
|
animation_schedule(sidebar_slide);
|
|
if (peek_anim) {
|
|
animation_schedule(peek_anim);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void prv_inactive_timer_callback(void *data) {
|
|
prv_set_state(data, TimelineAppStateInactive);
|
|
prv_exit(data);
|
|
}
|
|
|
|
static void prv_inactive_timer_refresh(TimelineAppData *data) {
|
|
static const uint32_t INACTIVITY_TIMEOUT_MS = 30 * 1000;
|
|
s_app_data->inactive_timer_id = evented_timer_register_or_reschedule(
|
|
s_app_data->inactive_timer_id, INACTIVITY_TIMEOUT_MS, prv_inactive_timer_callback, data);
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Pin View
|
|
/////////////////////////////////////
|
|
|
|
static void prv_move_timeline_layer_stopped(Animation *animation, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
|
|
// reset the timeline layer
|
|
data->timeline_layer.layer.bounds.origin.x = 0;
|
|
window_set_background_color(&data->timeline_window, GColorWhite);
|
|
|
|
if (!finished) {
|
|
return;
|
|
}
|
|
|
|
TimelineLayout *timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (!timeline_layout) {
|
|
return;
|
|
}
|
|
|
|
// cut to the card window
|
|
app_window_stack_push(&data->pin_window.window, false);
|
|
prv_set_state(data, TimelineAppStateCard);
|
|
|
|
TimelineIterState *state = timeline_model_get_current_state();
|
|
Uuid app_uuid;
|
|
timeline_get_originator_id(&state->pin, &app_uuid);
|
|
analytics_event_pin_open(state->pin.header.timestamp, &app_uuid);
|
|
}
|
|
|
|
static Animation *prv_animate_to_pin_window(TimelineAppData *data) {
|
|
Layer *layer = &data->timeline_layer.layer;
|
|
GPoint to_origin = GPoint(-layer->bounds.size.w, 0);
|
|
Animation *animation = (Animation *)property_animation_create_bounds_origin(layer, NULL,
|
|
&to_origin);
|
|
animation_set_handlers(animation, (AnimationHandlers) {
|
|
.stopped = prv_move_timeline_layer_stopped,
|
|
}, data);
|
|
animation_set_duration(animation, TIMELINE_CARD_TRANSITION_MS / 2);
|
|
animation_set_custom_interpolation(animation, interpolate_moook);
|
|
animation_schedule(animation);
|
|
return animation;
|
|
}
|
|
|
|
static void prv_push_pin_window(TimelineAppData *data, TimelineIterState *state, bool animated) {
|
|
TimelineLayout *timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (!timeline_layout) {
|
|
return;
|
|
}
|
|
|
|
// Animation structure:
|
|
// - Scheduled simultaneously
|
|
// - Transition pin to card
|
|
// - Move timeline layer to the left
|
|
|
|
animation_unschedule(data->current_animation);
|
|
|
|
// initialize the pin window with the card layout
|
|
timeline_pin_window_init(&data->pin_window, &state->pin, state->current_day);
|
|
|
|
// match the card background color
|
|
const LayoutColors *colors = layout_get_colors((LayoutLayer *)timeline_layout);
|
|
window_set_background_color(&data->timeline_window, colors->bg_color);
|
|
|
|
// animate the card from the right
|
|
#if !PLATFORM_TINTIN
|
|
TimelineLayout *card_timeline_layout = data->pin_window.item_detail_layer.timeline_layout;
|
|
timeline_layout_transition_pin_to_card(timeline_layout, card_timeline_layout);
|
|
#endif
|
|
|
|
// animate the timeline to the left
|
|
data->current_animation = prv_animate_to_pin_window(data);
|
|
}
|
|
|
|
static bool prv_pin_in_card(TimelineAppData *data, Uuid *uuid) {
|
|
if (!app_window_stack_contains_window((Window *)&data->pin_window)) {
|
|
return false;
|
|
}
|
|
|
|
TimelineIterState *current_state = timeline_model_get_current_state();
|
|
if (current_state == NULL) {
|
|
return false;
|
|
}
|
|
|
|
return uuid_equal(¤t_state->pin.header.id, uuid);
|
|
}
|
|
|
|
static void prv_refresh_pin(TimelineAppData *data, int idx) {
|
|
PBL_ASSERTN(idx >= 0);
|
|
TimelineIterState *state = timeline_model_get_iter_state(idx);
|
|
timeline_iter_refresh_pin(state);
|
|
|
|
if (idx == 0 && prv_pin_in_card(data, &state->pin.header.id)) {
|
|
timeline_pin_window_set_item(&data->pin_window, &state->pin, state->current_day);
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Timeline Controller
|
|
/////////////////////////////////////
|
|
|
|
T_STATIC void prv_setup_no_events_peek(TimelineAppData *data);
|
|
|
|
static void prv_update_timeline_layer(TimelineAppData *data) {
|
|
TimelineLayer *timeline_layer = &data->timeline_layer;
|
|
if (data->state != TimelineAppStateStationary &&
|
|
data->state != TimelineAppStateUpDown &&
|
|
data->state != TimelineAppStateCard) {
|
|
return;
|
|
}
|
|
animation_unschedule(data->current_animation);
|
|
data->current_animation = NULL;
|
|
timeline_layer_reset(timeline_layer);
|
|
|
|
if (timeline_model_is_empty() &&
|
|
prv_set_state(data, TimelineAppStateNoEvents)) {
|
|
// Hide layouts and animate to "No events"
|
|
timeline_layer_set_layouts_hidden(&data->timeline_layer, true);
|
|
|
|
prv_init_peek_layer(data);
|
|
prv_setup_no_events_peek(data);
|
|
peek_layer_play(&data->peek_layer);
|
|
|
|
Animation *sidebar_slide = prv_create_sidebar_animation(data, false /* open */);
|
|
data->current_animation = sidebar_slide;
|
|
animation_schedule(sidebar_slide);
|
|
}
|
|
}
|
|
|
|
static void prv_back_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
TimelineAppData *data = context;
|
|
prv_exit(data);
|
|
}
|
|
|
|
static void prv_up_down_stopped(Animation *animation, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
if (finished) {
|
|
prv_set_state(data, TimelineAppStateStationary);
|
|
}
|
|
}
|
|
|
|
static void prv_hide_day_sep_stopped(Animation *animation, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
if (!finished || !prv_set_state(data, TimelineAppStateStationary)) {
|
|
return;
|
|
}
|
|
|
|
data->current_animation = NULL;
|
|
prv_update_timeline_layer(data);
|
|
|
|
Animation *move_animation = timeline_layer_create_up_down_animation(
|
|
&data->timeline_layer, TIMELINE_UP_DOWN_ANIMATION_DURATION_MS / 2,
|
|
timeline_animation_interpolate_moook_second_half);
|
|
animation_set_handlers(move_animation, (AnimationHandlers) {
|
|
.stopped = prv_up_down_stopped,
|
|
}, data);
|
|
|
|
data->current_animation = move_animation;
|
|
animation_schedule(move_animation);
|
|
}
|
|
|
|
static void prv_hide_day_sep(void *context) {
|
|
TimelineAppData *data = context;
|
|
data->day_separator_timer_id = EVENTED_TIMER_INVALID_ID;
|
|
if (!prv_set_state(data, TimelineAppStateHideDaySeparator)) {
|
|
return;
|
|
}
|
|
|
|
animation_unschedule(data->current_animation);
|
|
|
|
Animation *day_sep_hide = timeline_layer_create_day_sep_hide(&data->timeline_layer);
|
|
animation_set_handlers(day_sep_hide, (AnimationHandlers){
|
|
.stopped = prv_hide_day_sep_stopped,
|
|
}, data);
|
|
data->current_animation = day_sep_hide;
|
|
animation_schedule(day_sep_hide);
|
|
}
|
|
|
|
static bool prv_attempt_hide_day_sep(TimelineAppData *data) {
|
|
if (data->state == TimelineAppStateDaySeparator) {
|
|
prv_cleanup_timer(&data->day_separator_timer_id);
|
|
prv_hide_day_sep(data);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void prv_select_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
TimelineAppData *data = context;
|
|
|
|
prv_attempt_hide_day_sep(data);
|
|
|
|
if (!prv_set_state(data, TimelineAppStatePushCard)) {
|
|
return;
|
|
}
|
|
|
|
TimelineIterState *state = timeline_model_get_current_state();
|
|
if (state) {
|
|
const bool animated = true;
|
|
prv_push_pin_window(data, state, animated);
|
|
}
|
|
}
|
|
|
|
static void prv_set_day_sep_timer(TimelineAppData *data) {
|
|
const int DAY_SEP_TIMEOUT_MS = 1000;
|
|
data->day_separator_timer_id = evented_timer_register(DAY_SEP_TIMEOUT_MS,
|
|
false,
|
|
prv_hide_day_sep,
|
|
data);
|
|
}
|
|
|
|
static void prv_day_sep_show_stopped(Animation *animation, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
if (!finished || !prv_set_state(data, TimelineAppStateDaySeparator)) {
|
|
return;
|
|
}
|
|
|
|
// Pins will reappear after the day separator completes hiding in `prv_hide_day_sep_stopped`
|
|
timeline_layer_set_layouts_hidden(&data->timeline_layer, true);
|
|
|
|
prv_set_day_sep_timer(data);
|
|
}
|
|
|
|
static void prv_up_down_click_handler(ClickRecognizerRef recognizer, void *context) {
|
|
TimelineAppData *data = context;
|
|
|
|
prv_inactive_timer_refresh(data);
|
|
|
|
ButtonId button = click_recognizer_get_button_id(recognizer);
|
|
const bool next = (button == BUTTON_ID_UP) ^
|
|
(data->timeline_model.direction == TimelineIterDirectionFuture);
|
|
|
|
// We want to know if it was stationary before transitioning
|
|
const bool was_stationary = (data->state == TimelineAppStateStationary);
|
|
|
|
if (data->state == TimelineAppStateNoEvents) {
|
|
if (!next) {
|
|
prv_exit(data);
|
|
}
|
|
return; // There are no events
|
|
} else if (prv_attempt_hide_day_sep(data)) {
|
|
return; // Successfully interrupted the day separator, let it hide
|
|
} else if (!prv_set_state(data, TimelineAppStateUpDown)) {
|
|
return; // Not in a state able to scroll at the moment
|
|
}
|
|
|
|
animation_unschedule(data->current_animation);
|
|
|
|
int new_idx;
|
|
bool has_new;
|
|
if (next) {
|
|
if (!timeline_model_iter_next(&new_idx, &has_new)) {
|
|
prv_set_state(data, TimelineAppStateStationary);
|
|
goto done;
|
|
}
|
|
if (has_new) {
|
|
timeline_layer_set_next_item(&data->timeline_layer, new_idx);
|
|
}
|
|
timeline_layer_move_data(&data->timeline_layer, 1);
|
|
} else {
|
|
if (!timeline_model_iter_prev(&new_idx, &has_new)) {
|
|
prv_exit(data);
|
|
goto done;
|
|
}
|
|
if (has_new) {
|
|
timeline_layer_set_prev_item(&data->timeline_layer, new_idx);
|
|
}
|
|
timeline_layer_move_data(&data->timeline_layer, -1);
|
|
}
|
|
|
|
// If we interrupted a previous scroll, hasten this scroll
|
|
const bool is_hasted = !was_stationary;
|
|
const uint32_t duration = TIMELINE_UP_DOWN_ANIMATION_DURATION_MS;
|
|
const InterpolateInt64Function interpolate = is_hasted ?
|
|
timeline_animation_interpolate_moook_second_half :
|
|
timeline_animation_interpolate_moook_soft;
|
|
Animation *move_animation = timeline_layer_create_up_down_animation(
|
|
&data->timeline_layer, duration, interpolate);
|
|
|
|
if (timeline_layer_should_animate_day_separator(&data->timeline_layer) &&
|
|
prv_set_state(data, TimelineAppStateShowDaySeparator)) {
|
|
Animation *day_sep_show = timeline_layer_create_day_sep_show(&data->timeline_layer);
|
|
move_animation = animation_spawn_create(move_animation, day_sep_show, NULL);
|
|
animation_set_handlers(move_animation, (AnimationHandlers) {
|
|
.stopped = prv_day_sep_show_stopped,
|
|
}, data);
|
|
} else {
|
|
animation_set_handlers(move_animation, (AnimationHandlers) {
|
|
.stopped = prv_up_down_stopped,
|
|
}, data);
|
|
}
|
|
|
|
data->current_animation = move_animation;
|
|
animation_schedule(move_animation);
|
|
|
|
done:
|
|
if (data->timeline_model.direction == TimelineIterDirectionPast) {
|
|
analytics_inc(ANALYTICS_DEVICE_METRIC_TIMELINE_PAST_NAVIGATION_COUNT, AnalyticsClient_System);
|
|
} else {
|
|
analytics_inc(ANALYTICS_DEVICE_METRIC_TIMELINE_FUTURE_NAVIGATION_COUNT, AnalyticsClient_System);
|
|
}
|
|
}
|
|
|
|
static void prv_click_config_provider(void *context) {
|
|
window_single_click_subscribe(BUTTON_ID_BACK, prv_back_click_handler);
|
|
window_single_click_subscribe(BUTTON_ID_UP, prv_up_down_click_handler);
|
|
window_single_click_subscribe(BUTTON_ID_DOWN, prv_up_down_click_handler);
|
|
window_single_click_subscribe(BUTTON_ID_SELECT, prv_select_click_handler);
|
|
}
|
|
|
|
static void prv_blobdb_event_handler(PebbleEvent *event, void *context) {
|
|
TimelineAppData *data = context;
|
|
PebbleBlobDBEvent *blobdb_event = &event->blob_db;
|
|
if (blobdb_event->db_id != BlobDBIdPins) {
|
|
// we only care about pins
|
|
return;
|
|
}
|
|
|
|
BlobDBEventType type = blobdb_event->type;
|
|
Uuid *id = (Uuid *)blobdb_event->key;
|
|
if (type == BlobDBEventTypeDelete) {
|
|
if (prv_pin_in_card(data, id)) {
|
|
// remove the pin window if we just removed the pin
|
|
app_window_stack_remove((Window *)&data->pin_window, false);
|
|
prv_set_state(data, TimelineAppStateStationary);
|
|
}
|
|
timeline_model_remove(id);
|
|
prv_update_timeline_layer(data);
|
|
} else if (type == BlobDBEventTypeInsert) {
|
|
for (int i = 0; i < TIMELINE_NUM_VISIBLE_ITEMS; i++) {
|
|
if (timeline_model_get_iter_state(i)->node &&
|
|
uuid_equal(&timeline_model_get_iter_state(i)->pin.header.id, id)) {
|
|
prv_refresh_pin(data, i);
|
|
}
|
|
}
|
|
prv_update_timeline_layer(data);
|
|
}
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Intro Animation
|
|
/////////////////////////////////////
|
|
|
|
static void prv_intro_anim_stopped(Animation *anim, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
i18n_free_all(&data->peek_layer);
|
|
peek_layer_deinit(&data->peek_layer);
|
|
window_set_click_config_provider_with_context(&data->timeline_window, prv_click_config_provider,
|
|
data);
|
|
data->timeline_layer.animating_intro_or_exit = false;
|
|
|
|
if (!finished || (!prv_set_state(s_app_data, TimelineAppStateStationary) &&
|
|
!prv_set_state(s_app_data, TimelineAppStateDaySeparator))) {
|
|
return;
|
|
}
|
|
|
|
data->current_animation = NULL;
|
|
prv_update_timeline_layer(data);
|
|
|
|
if (data->state == TimelineAppStateDaySeparator) {
|
|
// Hidden until the day separator hide animation stops
|
|
timeline_layer_set_layouts_hidden(&data->timeline_layer, true);
|
|
#if ANIMATION_DOT
|
|
timeline_layer_unfold_day_sep(&data->timeline_layer);
|
|
#elif ANIMATION_SLIDE
|
|
timeline_layer_slide_day_sep(&data->timeline_layer);
|
|
#endif
|
|
prv_set_day_sep_timer(data);
|
|
} else {
|
|
#if CAPABILITY_HAS_TIMELINE_PEEK
|
|
GPoint direction = GPoint(0, -1);
|
|
#else
|
|
GPoint direction = GPoint(1, 0);
|
|
#endif
|
|
Animation *layer_bounce = timeline_layer_create_bounce_back_animation(&data->timeline_layer,
|
|
direction);
|
|
data->current_animation = layer_bounce;
|
|
animation_schedule(layer_bounce);
|
|
}
|
|
}
|
|
|
|
static Animation *prv_create_intro_animation(TimelineAppData *data, uint32_t duration,
|
|
bool was_mini_peek) {
|
|
// Animation structure:
|
|
// - Scheduled simultaneously
|
|
// - Spawn
|
|
// - Move peek layer to right (frame)
|
|
// - Resize sidebar from fullscreen to thin
|
|
// - After completion
|
|
// - Bounce back timeline pin layouts
|
|
// - Speed lines (if launching into a deep pin)
|
|
|
|
// animate the peek layer to the right
|
|
GRect *start = &data->peek_layer.layer.frame;
|
|
GRect stop = { { was_mini_peek ? 0 : start->size.w , 0 }, start->size };
|
|
Animation *peek_out = (Animation *)property_animation_create_layer_frame(
|
|
(Layer *)&data->peek_layer, start, &stop);
|
|
animation_set_duration(peek_out, duration);
|
|
animation_set_custom_interpolation(peek_out, interpolate_moook_in_only);
|
|
|
|
// resize the sidebar from fullscreen to become thin on the right
|
|
Animation *sidebar_slide = prv_create_sidebar_animation(data, true /* open */);
|
|
animation_set_duration(sidebar_slide, duration);
|
|
|
|
Animation *speed_lines = data->launch_into_deep_pin ?
|
|
timeline_layer_create_speed_lines_animation(&data->timeline_layer) : NULL;
|
|
|
|
return animation_spawn_create(peek_out, sidebar_slide, speed_lines, NULL);
|
|
}
|
|
|
|
static void prv_play_peek_in(TimelineAppData *data) {
|
|
// Skip the first frame since the icon is offscreen
|
|
const int num_frames_skip = 1;
|
|
// The peek layer scale animation has a bounce back effect, so the icon reaches the destination
|
|
// if set to exactly the short moook in duration, so extend with more frames
|
|
const int num_frames_extend = 3;
|
|
const uint32_t duration = interpolate_moook_in_duration();
|
|
peek_layer_set_duration(&data->peek_layer, duration + ((num_frames_skip + num_frames_extend) *
|
|
ANIMATION_TARGET_FRAME_INTERVAL_MS));
|
|
Animation *animation = (Animation *)peek_layer_create_play_animation(&data->peek_layer);
|
|
animation_schedule(animation);
|
|
animation_set_elapsed(animation, num_frames_skip * ANIMATION_TARGET_FRAME_INTERVAL_MS);
|
|
}
|
|
|
|
static void prv_scale_peek_to_first_pin_icon(TimelineAppData *data, uint32_t duration,
|
|
bool was_mini_peek) {
|
|
TimelineLayout *first_timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (!first_timeline_layout) {
|
|
return;
|
|
}
|
|
|
|
// scale the peek layer icon to the pin position
|
|
GRect frame;
|
|
layer_get_global_frame((Layer *)first_timeline_layout, &frame);
|
|
GRect icon_to;
|
|
timeline_layout_get_icon_frame(&frame, data->timeline_layer.scroll_direction, &icon_to);
|
|
const bool align_in_frame = true;
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
peek_layer_set_scale_to_image(peek_layer, &first_timeline_layout->icon_info,
|
|
TimelineResourceSizeTiny, icon_to, align_in_frame);
|
|
|
|
#if ANIMATION_SLIDE
|
|
if (was_mini_peek) {
|
|
prv_play_peek_in(data);
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
peek_layer_set_duration(peek_layer, duration);
|
|
peek_layer_play(peek_layer);
|
|
}
|
|
|
|
static void prv_intro_timer_callback(void *context) {
|
|
TimelineAppData *data = context;
|
|
data->intro_timer_id = EVENTED_TIMER_INVALID_ID;
|
|
|
|
// if we are already hiding the peek, we were in a mini peek
|
|
const bool was_mini_peek = (data->state == TimelineAppStateHidePeek);
|
|
|
|
prv_set_state(data, TimelineAppStateHidePeek);
|
|
|
|
if (data->state != TimelineAppStateHidePeek &&
|
|
data->state != TimelineAppStateFarDayHidePeek) {
|
|
return;
|
|
}
|
|
|
|
// hide the peek text
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
peek_layer_clear_fields(peek_layer);
|
|
|
|
animation_unschedule(data->current_animation);
|
|
|
|
const uint32_t duration = was_mini_peek ? interpolate_moook_in_duration() :
|
|
TIMELINE_SLIDE_ANIMATION_MS;
|
|
Animation *intro = prv_create_intro_animation(data, duration, was_mini_peek);
|
|
animation_set_handlers(intro, (AnimationHandlers) {
|
|
.started = prv_intro_or_exit_anim_started,
|
|
.stopped = prv_intro_anim_stopped,
|
|
}, data);
|
|
|
|
data->current_animation = intro;
|
|
animation_schedule(intro);
|
|
|
|
if (!layer_get_hidden((Layer *)&data->peek_layer)) {
|
|
prv_scale_peek_to_first_pin_icon(data, duration, was_mini_peek);
|
|
}
|
|
}
|
|
|
|
static void prv_open_did_focus_handler(PebbleEvent *e, void *context) {
|
|
TimelineAppData *data = context;
|
|
event_service_client_unsubscribe(&data->focus_event_info);
|
|
prv_intro_timer_callback(data);
|
|
}
|
|
|
|
static void prv_peek_did_focus_handler(PebbleEvent *e, void *context) {
|
|
TimelineAppData *data = context;
|
|
event_service_client_unsubscribe(&data->focus_event_info);
|
|
|
|
#if ANIMATION_DOT
|
|
peek_layer_play(&data->peek_layer);
|
|
#elif ANIMATION_SLIDE
|
|
prv_play_peek_in(data);
|
|
#endif
|
|
|
|
if (data->state == TimelineAppStateNoEvents) {
|
|
window_set_click_config_provider_with_context(&data->timeline_window, prv_click_config_provider,
|
|
data);
|
|
} else if (data->state == TimelineAppStatePeek &&
|
|
data->intro_timer_id == EVENTED_TIMER_INVALID_ID) {
|
|
data->intro_timer_id = evented_timer_register(PEEK_SHOW_TIME_MS,
|
|
false,
|
|
prv_intro_timer_callback,
|
|
data);
|
|
}
|
|
}
|
|
|
|
static void prv_setup_peek_animation(TimelineAppData *data, TimelineResourceInfo *timeline_res,
|
|
bool use_pin) {
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
#if ANIMATION_DOT
|
|
peek_layer_set_icon(peek_layer, timeline_res);
|
|
#elif ANIMATION_SLIDE
|
|
GRect icon_from;
|
|
GRect icon_to;
|
|
const bool shift_offscreen_from = true;
|
|
const bool shift_offscreen_to = false;
|
|
prv_get_icon_animation_frame(data, &icon_from, use_pin, shift_offscreen_from);
|
|
prv_get_icon_animation_frame(data, &icon_to, use_pin, shift_offscreen_to);
|
|
peek_layer_set_icon_with_size(peek_layer, timeline_res, TimelineResourceSizeLarge, icon_from);
|
|
peek_layer_set_scale_to(peek_layer, icon_to);
|
|
peek_layer_set_fields_hidden(peek_layer, true);
|
|
#endif
|
|
}
|
|
|
|
T_STATIC void prv_setup_no_events_peek(TimelineAppData *data) {
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
// set the text
|
|
peek_layer_set_fields(peek_layer, "", i18n_get("No events", peek_layer), "");
|
|
// set the icon resource
|
|
TimelineResourceInfo timeline_res = {
|
|
.res_id = TIMELINE_RESOURCE_NO_EVENTS,
|
|
};
|
|
const bool use_pin = false;
|
|
prv_setup_peek_animation(data, &timeline_res, use_pin);
|
|
}
|
|
|
|
static void prv_setup_first_pin_peek(TimelineAppData *data) {
|
|
TimelineIterState *state = timeline_model_get_current_state();
|
|
// TODO: PBL-22075 Refactor Timeline Model
|
|
// timeline_model_get_current_state explicitly tries to return NULL when supposedly empty,
|
|
// but this does not seem to actually happen
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
TimelineItem *first_pin = &state->pin;
|
|
if (!first_pin) {
|
|
return;
|
|
}
|
|
|
|
TimelineLayout *first_timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (!first_timeline_layout) {
|
|
return;
|
|
}
|
|
|
|
PeekLayer *peek_layer = &s_app_data->peek_layer;
|
|
|
|
#if PLATFORM_TINTIN
|
|
const bool is_mini_peek = false;
|
|
#else
|
|
// if we are hiding the peek, we are in a mini peek
|
|
const bool is_mini_peek = (data->state == TimelineAppStateHidePeek);
|
|
#endif
|
|
|
|
// set the text
|
|
char number_buffer[TIME_STRING_REQUIRED_LENGTH] = {}; // "11"
|
|
char word_buffer[TIME_STRING_REQUIRED_LENGTH] = {}; // "min to"
|
|
if (!is_mini_peek) {
|
|
clock_get_event_relative_time_string(
|
|
number_buffer, sizeof(number_buffer), word_buffer, sizeof(word_buffer),
|
|
first_pin->header.timestamp, first_pin->header.duration, state->current_day,
|
|
first_pin->header.all_day);
|
|
}
|
|
peek_layer_set_fields(peek_layer, number_buffer, word_buffer, "");
|
|
|
|
// set the icon
|
|
if (is_mini_peek) {
|
|
GRect icon_from;
|
|
const bool shift_offscreen = true;
|
|
const bool use_pin = true;
|
|
prv_get_icon_animation_frame(data, &icon_from, use_pin, shift_offscreen);
|
|
peek_layer_set_icon_with_size(peek_layer, &first_timeline_layout->icon_info,
|
|
TimelineResourceSizeTiny, icon_from);
|
|
} else {
|
|
const bool use_pin = false;
|
|
prv_setup_peek_animation(data, &first_timeline_layout->icon_info, use_pin);
|
|
}
|
|
}
|
|
|
|
static void NOINLINE prv_setup_peek(TimelineAppData *data) {
|
|
TimelineIterState *state = timeline_model_get_current_state();
|
|
TimelineItem *first_pin = state ? &state->pin : NULL;
|
|
EventServiceEventHandler focus_handler = prv_open_did_focus_handler;
|
|
|
|
// we'll only show the first pin peek if timeline peek (aka quick view) isn't enabled
|
|
time_t now = rtc_get_time();
|
|
if (!first_pin && prv_set_state(s_app_data, TimelineAppStateNoEvents)) {
|
|
layer_set_hidden((Layer *)&data->peek_layer, false);
|
|
prv_setup_no_events_peek(data);
|
|
focus_handler = prv_peek_did_focus_handler;
|
|
#if !CAPABILITY_HAS_TIMELINE_PEEK
|
|
} else if (first_pin && (first_pin->header.timestamp + SECONDS_PER_MINUTE *
|
|
first_pin->header.duration >= now) &&
|
|
(first_pin->header.timestamp - SECONDS_PER_HOUR <= now) &&
|
|
prv_set_state(data, TimelineAppStatePeek)) {
|
|
// ongoing or within the hour
|
|
prv_setup_first_pin_peek(data);
|
|
focus_handler = prv_peek_did_focus_handler;
|
|
#endif
|
|
} else if (state && (state->current_day != time_util_get_midnight_of(now)) &&
|
|
prv_set_state(data, TimelineAppStateFarDayHidePeek)) {
|
|
// entering into a day that isn't today, setup the day separator
|
|
layer_set_hidden((Layer *)&data->peek_layer, true);
|
|
#if ANIMATION_DOT
|
|
timeline_layer_set_day_sep_frame(&data->timeline_layer, &data->timeline_layer.layer.frame);
|
|
#elif ANIMATION_SLIDE
|
|
GRect frame;
|
|
layer_get_frame(&data->timeline_layer.day_separator.layer, &frame);
|
|
const bool is_future = (s_app_data->timeline_model.direction == TimelineIterDirectionFuture);
|
|
frame.origin.y += is_future ? DISP_ROWS : -DISP_ROWS;
|
|
timeline_layer_set_day_sep_frame(&data->timeline_layer, &frame);
|
|
#endif
|
|
focus_handler = prv_open_did_focus_handler;
|
|
} else if (prv_set_state(data, TimelineAppStateHidePeek)) {
|
|
// setup mini-peek where the icon animates directly into the pin position
|
|
prv_setup_first_pin_peek(data);
|
|
focus_handler = prv_open_did_focus_handler;
|
|
}
|
|
|
|
// set the did_focus handler
|
|
data->focus_event_info = (EventServiceInfo) {
|
|
.type = PEBBLE_APP_DID_CHANGE_FOCUS_EVENT,
|
|
.handler = focus_handler,
|
|
.context = s_app_data,
|
|
};
|
|
event_service_client_subscribe(&s_app_data->focus_event_info);
|
|
}
|
|
|
|
static GColor prv_get_sidebar_color(TimelineAppData *data) {
|
|
if (s_app_data->timeline_model.direction == TimelineIterDirectionPast) {
|
|
return TIMELINE_PAST_COLOR;
|
|
} else {
|
|
return TIMELINE_FUTURE_COLOR;
|
|
}
|
|
}
|
|
|
|
T_STATIC void prv_init_peek_layer(TimelineAppData *data) {
|
|
Window *window = &data->timeline_window;
|
|
PeekLayer *peek_layer = &data->peek_layer;
|
|
const TimelineAppStyle *style = prv_get_style();
|
|
const GRect frame = { .origin.y = style->peek_offset_y, .size = window->layer.bounds.size };
|
|
peek_layer_init(peek_layer, &frame);
|
|
peek_layer_set_icon_offset_y(peek_layer, style->peek_icon_offset_y);
|
|
peek_layer_set_frame(peek_layer, &frame);
|
|
peek_layer_set_background_color(peek_layer, GColorClear);
|
|
layer_add_child(&window->layer, &peek_layer->layer);
|
|
}
|
|
|
|
static void prv_timeline_window_load(Window *window) {
|
|
TimelineLayer *timeline_layer = &s_app_data->timeline_layer;
|
|
TimelineScrollDirection scroll_direction;
|
|
if (s_app_data->timeline_model.direction == TimelineIterDirectionPast) {
|
|
scroll_direction = TimelineScrollDirectionUp;
|
|
} else {
|
|
scroll_direction = TimelineScrollDirectionDown;
|
|
}
|
|
|
|
window_set_background_color(window, GColorWhite);
|
|
|
|
// timeline layer
|
|
timeline_layer_init(timeline_layer, &window->layer.bounds, scroll_direction);
|
|
timeline_layer_set_sidebar_color(timeline_layer,
|
|
PBL_IF_COLOR_ELSE(prv_get_sidebar_color(s_app_data),
|
|
GColorLightGray));
|
|
timeline_layer_set_layouts_hidden(timeline_layer, true); // hide until the peek is over
|
|
layer_set_hidden((Layer *)&timeline_layer->day_separator, true);
|
|
layer_add_child(&window->layer, (Layer *)timeline_layer);
|
|
|
|
// peek layer
|
|
prv_init_peek_layer(s_app_data);
|
|
prv_setup_peek(s_app_data);
|
|
}
|
|
|
|
static void prv_timeline_window_appear(Window *window) {
|
|
TimelineAppData *data = window_get_user_data(window);
|
|
|
|
// re-enable the inactivity timer back in timeline view
|
|
prv_inactive_timer_refresh(data);
|
|
}
|
|
|
|
static void prv_timeline_window_disappear(Window *window) {
|
|
TimelineAppData *data = window_get_user_data(window);
|
|
|
|
// disable the inactivity timer when the user leaves
|
|
prv_cleanup_timer(&data->inactive_timer_id);
|
|
}
|
|
|
|
static void prv_timeline_window_unload(Window *window) {
|
|
TimelineAppData *data = window_get_user_data(window);
|
|
|
|
// clean up any running animations
|
|
animation_unschedule(data->current_animation);
|
|
prv_cleanup_timer(&data->day_separator_timer_id);
|
|
}
|
|
|
|
static void prv_back_from_card_stopped(Animation *animation, bool finished, void *context) {
|
|
TimelineAppData *data = context;
|
|
if (!finished || !prv_set_state(data, TimelineAppStateStationary)) {
|
|
return;
|
|
}
|
|
|
|
window_set_background_color(&data->timeline_window, GColorWhite);
|
|
|
|
data->current_animation = NULL;
|
|
prv_update_timeline_layer(data);
|
|
|
|
Animation *layer_bounce = timeline_layer_create_bounce_back_animation(&data->timeline_layer,
|
|
GPoint(1, 0));
|
|
|
|
data->current_animation = layer_bounce;
|
|
animation_schedule(layer_bounce);
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// Public API
|
|
/////////////////////////////////////
|
|
|
|
Animation *timeline_animate_back_from_card(void) {
|
|
TimelineAppData *data = s_app_data;
|
|
PBL_ASSERTN(data);
|
|
|
|
if (!prv_set_state(data, TimelineAppStatePopCard)) {
|
|
return NULL;
|
|
}
|
|
|
|
// Animation structure:
|
|
// - Scheduled simultaneously
|
|
// - Transition card to pin
|
|
// - Move timeline layer from the left
|
|
// - After completion
|
|
// - Bounce back the timeline layer
|
|
// - Move pin window to the right
|
|
|
|
animation_unschedule(data->current_animation);
|
|
|
|
timeline_layer_set_layouts_hidden(&data->timeline_layer, true);
|
|
window_set_background_color(&data->timeline_window, GColorWhite);
|
|
|
|
|
|
#if !PLATFORM_TINTIN
|
|
TimelineLayout *pin_timeline_layout = timeline_layer_get_current_layout(&data->timeline_layer);
|
|
if (pin_timeline_layout) {
|
|
// animation the pin icon
|
|
TimelineItemLayer *item_layer = &data->pin_window.item_detail_layer;
|
|
timeline_layout_transition_card_to_pin(item_layer->timeline_layout, pin_timeline_layout);
|
|
}
|
|
#endif
|
|
|
|
// animate the timeline layer from the left
|
|
Layer *layer = &data->timeline_layer.layer;
|
|
GPoint from_origin = { -layer->bounds.size.w, 0 };
|
|
Animation *layer_in = (Animation *)property_animation_create_bounds_origin(layer, &from_origin,
|
|
&GPointZero);
|
|
animation_set_duration(layer_in, TIMELINE_CARD_TRANSITION_MS / 2);
|
|
animation_set_custom_interpolation(layer_in, interpolate_moook);
|
|
animation_set_handlers(layer_in, (AnimationHandlers) {
|
|
.stopped = prv_back_from_card_stopped,
|
|
}, data);
|
|
|
|
data->current_animation = layer_in;
|
|
animation_schedule(layer_in);
|
|
|
|
// animate the card layout
|
|
timeline_pin_window_pop(&data->pin_window);
|
|
|
|
return layer_in;
|
|
}
|
|
|
|
/////////////////////////////////////
|
|
// App boilerplate
|
|
/////////////////////////////////////
|
|
|
|
static bool NOINLINE prv_setup_timeline_app(void) {
|
|
TimelineAppData *data = app_malloc_check(sizeof(TimelineAppData));
|
|
s_app_data = data;
|
|
*data = (TimelineAppData){};
|
|
|
|
data->blobdb_event_info = (EventServiceInfo) {
|
|
.type = PEBBLE_BLOBDB_EVENT,
|
|
.handler = prv_blobdb_event_handler,
|
|
.context = data,
|
|
};
|
|
event_service_client_subscribe(&data->blobdb_event_info);
|
|
|
|
const TimelineArgs *args = process_manager_get_current_process_args();
|
|
Uuid app_uuid;
|
|
sys_get_app_uuid(&app_uuid);
|
|
if (uuid_equal(&app_uuid, &(Uuid)TIMELINE_PAST_UUID_INIT)) {
|
|
data->timeline_model.direction = TimelineIterDirectionPast;
|
|
} else if (args == NULL) {
|
|
data->timeline_model.direction = TimelineIterDirectionFuture;
|
|
} else {
|
|
data->timeline_model.direction = args->direction;
|
|
}
|
|
|
|
// check if we were asked to launch into a specific item
|
|
time_t now = rtc_get_time();
|
|
TimelineItem pin;
|
|
bool launch_into_pin = false;
|
|
if (args && args->launch_into_pin &&
|
|
!uuid_is_invalid(&args->pin_id) &&
|
|
pin_db_get(&args->pin_id, &pin) == S_SUCCESS) {
|
|
launch_into_pin = true;
|
|
if (!args->stay_in_list_view) {
|
|
// Launching directly into the pin, change the direction to match
|
|
data->timeline_model.direction =
|
|
timeline_direction_for_item(&pin, data->timeline_model.timeline, now);
|
|
}
|
|
timeline_item_free_allocated_buffer(&pin);
|
|
}
|
|
|
|
timeline_model_init(now, &data->timeline_model);
|
|
|
|
// if we're launching into a particular item, we iterate to it now
|
|
if (launch_into_pin) {
|
|
while (!uuid_equal(&timeline_model_get_current_state()->pin.header.id, &args->pin_id)) {
|
|
data->launch_into_deep_pin = true;
|
|
if (!timeline_model_iter_next(NULL, NULL)) {
|
|
// for some reason we can't find the pin we were asked to launch into
|
|
char uuid_buffer[UUID_STRING_BUFFER_LENGTH];
|
|
uuid_to_string(&args->pin_id, uuid_buffer);
|
|
PBL_LOG(LOG_LEVEL_ERROR, "Asked to launch into pin but can't find it %s", uuid_buffer);
|
|
launch_into_pin = false;
|
|
data->launch_into_deep_pin = false;
|
|
// we couldn't find the launch pin, go back to the present
|
|
while (timeline_model_iter_prev(NULL, NULL)) {}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Window *window = &data->timeline_window;
|
|
window_init(window, WINDOW_NAME("Timeline"));
|
|
window_set_user_data(window, data);
|
|
window_set_window_handlers(window, &(WindowHandlers) {
|
|
.load = prv_timeline_window_load,
|
|
.appear = prv_timeline_window_appear,
|
|
.disappear = prv_timeline_window_disappear,
|
|
.unload = prv_timeline_window_unload
|
|
});
|
|
|
|
return (launch_into_pin && !(args && args->stay_in_list_view));
|
|
}
|
|
|
|
T_STATIC void NOINLINE prv_init(void) {
|
|
bool do_push_pin_window = prv_setup_timeline_app();
|
|
|
|
app_window_stack_push(&s_app_data->timeline_window, true /* animated */);
|
|
|
|
if (do_push_pin_window) {
|
|
prv_push_pin_window(s_app_data, timeline_model_get_current_state(), false /* animated */);
|
|
}
|
|
|
|
#if CAPABILITY_HAS_TIMELINE_PEEK
|
|
if (!timeline_model_is_empty()) {
|
|
timeline_layer_set_sidebar_width(&s_app_data->timeline_layer,
|
|
timeline_layer_get_ideal_sidebar_width());
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void NOINLINE prv_deinit(void) {
|
|
prv_cleanup_timer(&s_app_data->intro_timer_id);
|
|
prv_cleanup_timer(&s_app_data->inactive_timer_id);
|
|
|
|
event_service_client_unsubscribe(&s_app_data->focus_event_info);
|
|
event_service_client_unsubscribe(&s_app_data->blobdb_event_info);
|
|
|
|
timeline_layer_deinit(&s_app_data->timeline_layer);
|
|
timeline_model_deinit();
|
|
app_free(s_app_data);
|
|
}
|
|
|
|
static void prv_main(void) {
|
|
prv_init();
|
|
|
|
app_event_loop();
|
|
|
|
prv_deinit();
|
|
}
|
|
|
|
const PebbleProcessMd *timeline_get_app_info() {
|
|
static const PebbleProcessMdSystem s_app_md = {
|
|
.common = {
|
|
.main_func = prv_main,
|
|
// uuid: 79C76B48-6111-4E80-8DEB-3119EEBEF33E
|
|
.uuid = {0x79, 0xC7, 0x6B, 0x48, 0x61, 0x11, 0x4E, 0x80,
|
|
0x8D, 0xEB, 0x31, 0x19, 0xEE, 0xBE, 0xF3, 0x3E},
|
|
.visibility = ProcessVisibilityHidden,
|
|
},
|
|
.name = "Timeline",
|
|
};
|
|
return &s_app_md.common;
|
|
}
|
|
|
|
const PebbleProcessMd *timeline_past_get_app_info() {
|
|
static const PebbleProcessMdSystem s_app_md = {
|
|
.common = {
|
|
.main_func = prv_main,
|
|
.uuid = TIMELINE_PAST_UUID_INIT,
|
|
.visibility = ProcessVisibilityQuickLaunch,
|
|
},
|
|
/// The title of Timeline Past in Quick Launch. If the translation is too long, cut out
|
|
/// Timeline and only translate "Past".
|
|
.name = i18n_noop("Timeline Past"),
|
|
};
|
|
return &s_app_md.common;
|
|
}
|