text:
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.compositor.bottombar;

import android.content.Context;
import android.graphics.Color;
import android.os.Handler;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.PanelState;
import org.chromium.chrome.browser.compositor.bottombar.OverlayPanel.StateChangeReason;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchOptOutPromo;
import org.chromium.chrome.browser.compositor.bottombar.contextualsearch.ContextualSearchOptOutPromo.ContextualSearchPromoHost;
import org.chromium.chrome.browser.preferences.PreferencesLauncher;
import org.chromium.chrome.browser.preferences.privacy.ContextualSearchPreferenceFragment;
import org.chromium.chrome.browser.util.MathUtils;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.resources.dynamics.DynamicResourceLoader;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Base abstract class for the Overlay Panel.
 * TODO(mdjones): Remove contextual search dependencies from this class.
 */
abstract class OverlayPanelBase implements ContextualSearchPromoHost {
    /** The side padding of Bar icons in dps. */
    private static final float BAR_ICON_SIDE_PADDING_DP = 12.f;

    /** The height of the Bar's border in dps. */
    private static final float BAR_BORDER_HEIGHT_DP = 1.f;

    /** The height of the expanded Overlay Panel relative to the height of the screen. */
    private static final float EXPANDED_PANEL_HEIGHT_PERCENTAGE = .7f;

    /** The width of the small version of the Overlay Panel in dps. */
    private static final float SMALL_PANEL_WIDTH_DP = 600.f;

    /**
     * The minimum width a screen should have in order to trigger the small version of the Panel.
     */
    private static final float SMALL_PANEL_WIDTH_THRESHOLD_DP = 680.f;

    /** The brightness of the base page when the Panel is peeking. */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_PEEKED = 1.f;

    /** The brightness of the base page when the Panel is expanded. */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_EXPANDED = .7f;

    /**
     * The brightness of the base page when the Panel is maximized. This value matches the alert
     * dialog brightness filter.
     */
    private static final float BASE_PAGE_BRIGHTNESS_STATE_MAXIMIZED = .4f;

    /** The opacity of the arrow icon when the Panel is peeking. */
    private static final float ARROW_ICON_OPACITY_STATE_PEEKED = 1.f;

    /** The opacity of the arrow icon when the Panel is expanded. */
    private static final float ARROW_ICON_OPACITY_STATE_EXPANDED = 0.f;

    /** The opacity of the arrow icon when the Panel is maximized. */
    private static final float ARROW_ICON_OPACITY_STATE_MAXIMIZED = 0.f;

    /** The rotation of the arrow icon. */
    private static final float ARROW_ICON_ROTATION = -90.f;

    /** The opacity of the close icon when the Panel is peeking. */
    private static final float CLOSE_ICON_OPACITY_STATE_PEEKED = 0.f;

    /** The opacity of the close icon when the Panel is expanded. */
    private static final float CLOSE_ICON_OPACITY_STATE_EXPANDED = 1.f;

    /** The opacity of the close icon when the Panel is maximized. */
    private static final float CLOSE_ICON_OPACITY_STATE_MAXIMIZED = 1.f;

    /** The id of the close icon drawable. */
    public static final int CLOSE_ICON_DRAWABLE_ID = R.drawable.btn_close;

    /** The height of the Progress Bar in dps. */
    private static final float PROGRESS_BAR_HEIGHT_DP = 2.f;

    /**
     * The distance from the Progress Bar must be away from the bottom of the
     * screen in order to be completely visible. The closer the Progress Bar
     * gets to the bottom of the screen, the lower its opacity will be. When the
     * Progress Bar is at the very bottom of the screen (when the Overlay Panel
     * is peeking) it will be completely invisible.
     */
    private static final float PROGRESS_BAR_VISIBILITY_THRESHOLD_DP = 10.f;

    /** The height of the Toolbar in dps. */
    private float mToolbarHeight;

    /** The height of the Bar when the Panel is peeking, in dps. */
    private float mBarHeightPeeking;

    /** The height of the Bar when the Panel is expanded, in dps. */
    private float mBarHeightExpanded;

    /** The height of the Bar when the Panel is maximized, in dps. */
    private float mBarHeightMaximized;

    /** Ratio of dps per pixel. */
    protected float mPxToDp;

    /** The approximate Y coordinate of the selection in pixels. */
    private float mBasePageSelectionYPx = -1.f;

    /**
     * The Y coordinate to apply to the Base Page in order to keep the selection
     * in view when the Overlay Panel is in its EXPANDED state.
     */
    private float mBasePageTargetY = 0.f;

    /** Whether the Panel is showing. */
    private boolean mIsShowing;

    /** The current context. */
    protected final Context mContext;

    /** The current state of the Overlay Panel. */
    private PanelState mPanelState = PanelState.UNDEFINED;

    /**
     * Valid previous states for the Panel.
     */
    protected static final Map<PanelState, PanelState> PREVIOUS_STATES;
    static {
        Map<PanelState, PanelState> states = new HashMap<PanelState, PanelState>();
        // Pairs are of the form <Current, Previous>.
        states.put(PanelState.PEEKED, PanelState.CLOSED);
        states.put(PanelState.EXPANDED, PanelState.PEEKED);
        states.put(PanelState.MAXIMIZED, PanelState.EXPANDED);
        PREVIOUS_STATES = Collections.unmodifiableMap(states);
    }

    // ============================================================================================
    // Constructor
    // ============================================================================================

    /**
     * @param context The current Android {@link Context}.
     */
    public OverlayPanelBase(Context context) {
        mContext = context;
    }

    // ============================================================================================
    // General API
    // ============================================================================================

    /**
     * Animates the Overlay Panel to its closed state.
     * @param reason The reason for the change of panel state.
     * @param animate If the panel should animate closed.
     */
    protected abstract void closePanel(StateChangeReason reason, boolean animate);

    /**
     * Animates the acceptance of the Promo.
     */
    protected abstract void animatePromoAcceptance();

    /**
     * Animates the search term resolution.
     */
    protected abstract void animateSearchTermResolution();

    /**
     * Cancels the search term resolution animation if it is in progress.
     */
    protected abstract void cancelSearchTermResolutionAnimation();

    /**
     * Event notification that the Panel did get closed.
     * @param reason The reason the panel is closing.
     */
    protected abstract void onClosed(StateChangeReason reason);

    /**
     * @return The absolute amount in DP that the top controls have shifted off screen.
     */
    protected abstract float getTopControlsOffsetDp();

    /**
     * TODO(mdjones): This method should be removed from this class.
     * @return The resource id that contains how large the top controls are.
     */
    protected abstract int getControlContainerHeightResource();

    // ============================================================================================
    // Peeking promo methods
    // ============================================================================================

    /**
     * @return The height of the Peek Promo in the peeked state, in pixels.
     */
    protected float getPeekPromoHeightPeekingPx() {
        return 0;
    }

    /**
     * @return The height of the Peek Promo, in pixels.
     */
    protected float getPeekPromoHeight() {
        return 0;
    }

    // ============================================================================================
    // Layout Integration
    // ============================================================================================

    private float mLayoutWidth;
    private float mLayoutHeight;

    private float mMaximumWidth;
    private float mMaximumHeight;

    private boolean mIsFullWidthSizePanelForTesting;
    private boolean mOverrideIsFullWidthSizePanelForTesting;

    /**
     * Called when the size of the view has changed.
     *
     * @param width  The new width in dp.
     * @param height The new width in dp.
     */
    public void onSizeChanged(float width, float height) {
        mLayoutWidth = width;
        mLayoutHeight = height;

        mMaximumWidth = calculateOverlayPanelWidth();
        mMaximumHeight = getPanelHeightFromState(PanelState.MAXIMIZED);

        // If the panel is closed, the height and offsets aren't changing. In a few tests, we get
        // calls to onSizeChanged() when the panel isn't fully initialized, which causes the tests
        // to fail if we try to call setPanelHeight().
        if (mPanelState != PanelState.UNDEFINED && mPanelState != PanelState.CLOSED) {
            setPanelHeight(getPanelHeightFromState(getPanelState()));
            mOffsetX = calculateOverlayPanelX();
            mOffsetY = calculateOverlayPanelY();
        }
    }

    /**
     * Overrides the FullWidthSizePanel state for testing.
     *
     * @param isFullWidthSizePanel
     */
    @VisibleForTesting
    public void setIsFullWidthSizePanelForTesting(boolean isFullWidthSizePanel) {
        mOverrideIsFullWidthSizePanelForTesting = true;
        mIsFullWidthSizePanelForTesting = isFullWidthSizePanel;
    }

    /**
     * @return Whether the Panel is in full width size.
     */
    protected boolean isFullWidthSizePanel() {
        if (mOverrideIsFullWidthSizePanelForTesting) {
            return mIsFullWidthSizePanelForTesting;
        }
        return getFullscreenWidth() <= SMALL_PANEL_WIDTH_THRESHOLD_DP;
    }

    /**
     * @return Whether the narrow version of the Panel is supported, in any orientation.
     */
    protected boolean isNarrowSizePanelSupported() {
        return !isFullWidthSizePanel() || getTabHeight() > SMALL_PANEL_WIDTH_THRESHOLD_DP;
    }

    /**
     * @return The current X-position of the Overlay Panel.
     */
    protected float calculateOverlayPanelX() {
        return isFullWidthSizePanel() ? 0.f
                : Math.round((getFullscreenWidth() - calculateOverlayPanelWidth()) / 2.f);
    }

    /**
     * @return The current Y-position of the Overlay Panel.
     */
    protected float calculateOverlayPanelY() {
        return getTabHeight() - mHeight;
    }

    /**
     * @return The current width of the Overlay Panel.
     */
    protected float calculateOverlayPanelWidth() {
        return isFullWidthSizePanel() ? getFullscreenWidth() : SMALL_PANEL_WIDTH_DP;
    }

    /**
     * @return The height of the Chrome toolbar in dp.
     */
    public float getToolbarHeight() {
        return mToolbarHeight;
    }

    /**
     * @param y The y coordinate.
     * @return The Y coordinate relative the fullscreen height.
     */
    public float getFullscreenY(float y) {
        return y + (mToolbarHeight - getTopControlsOffsetDp()) / mPxToDp;
    }

    /**
     * @return Whether the Panel is showing.
     */
    public boolean isShowing() {
        return mIsShowing;
    }

    /**
     * @return Whether the Overlay Panel is opened. That is, whether it is EXPANDED or MAXIMIZED.
     */
    public boolean isPanelOpened() {
        return mPanelState == PanelState.EXPANDED || mPanelState == PanelState.MAXIMIZED;
    }

    /**
     * Starts showing the Panel.
     */
    protected void startShowing() {
        mIsShowing = true;
    }

    /**
     * @return The fullscreen width.
     */
    private float getFullscreenWidth() {
        return mLayoutWidth;
    }

    /**
     * @return The height of the tab the panel is displayed on top of.
     */
    public float getTabHeight() {
        // NOTE(mdjones): This value will always be the same for a particular orientation; it is
        // the content height + visible toolbar height.
        return mLayoutHeight + (getToolbarHeight() - getTopControlsOffsetDp());
    }

    /**
     * @return The maximum height of the Overlay Panel in dps.
     */
    public float getMaximumPanelHeight() {
        return mMaximumHeight;
    }

    /**
     * @return The maximum width of the Overlay Panel in pixels.
     */
    public int getMaximumWidthPx() {
        return Math.round(mMaximumWidth / mPxToDp);
    }

    /**
     * @return The maximum height of the Overlay Panel in pixels.
     */
    public int getMaximumHeightPx() {
        return Math.round(mMaximumHeight / mPxToDp);
    }

    /**
     * The Panel Bar Container is a abstract container that groups the Controls
     * that will be visible when the Panel is in the peeked state.
     *
     * @return The Panel Bar Container in dps.
     */
    protected float getBarContainerHeight() {
        return getBarHeight() + getPeekPromoHeight();
    }

    /**
     * @return The width of the Overlay Panel Content View in pixels.
     */
    public int getContentViewWidthPx() {
        return getMaximumWidthPx();
    }

    /**
     * @return The height of the Overlay Panel Content View in pixels.
     */
    public int getContentViewHeightPx() {
        float barExpandedHeight = isFullWidthSizePanel()
                ? getToolbarHeight() : mBarHeightPeeking;
        return Math.round((mMaximumHeight - barExpandedHeight) / mPxToDp);
    }

    // ============================================================================================
    // UI States
    // ============================================================================================

    // --------------------------------------------------------------------------------------------
    // Test Infrastructure
    // --------------------------------------------------------------------------------------------

    /**
     * @param height The height of the Overlay Panel to be set.
     */
    @VisibleForTesting
    public void setHeightForTesting(float height) {
        mHeight = height;
    }

    /**
     * @param offsetY The vertical offset of the Overlay Panel to be
     *            set.
     */
    @VisibleForTesting
    public void setOffsetYForTesting(float offsetY) {
        mOffsetY = offsetY;
    }

    /**
     * @param isMaximized The setting for whether the Overlay Panel is fully
     *            maximized.
     */
    @VisibleForTesting
    public void setMaximizedForTesting(boolean isMaximized) {
        mIsMaximized = isMaximized;
    }

    /**
     * @param barHeight The height of the Overlay Bar to be set.
     */
    @VisibleForTesting
    public void setSearchBarHeightForTesting(float barHeight) {
        mBarHeight = barHeight;
    }

    /**
     * @param barBorderHeight The height of the Bar border to be set.
     */
    @VisibleForTesting
    public void setSearchBarBorderHeight(float barBorderHeight) {
        mBarBorderHeight = barBorderHeight;
    }

    // --------------------------------------------------------------------------------------------
    // Overlay Panel states
    // --------------------------------------------------------------------------------------------

    private float mOffsetX;
    private float mOffsetY;
    private float mHeight;
    private boolean mIsMaximized;

    /**
     * @return The vertical offset of the Overlay Panel.
     */
    public float getOffsetX() {
        return mOffsetX;
    }

    /**
     * @return The vertical offset of the Overlay Panel.
     */
    public float getOffsetY() {
        return mOffsetY;
    }

    /**
     * @return The width of the Overlay Panel in dps.
     */
    public float getWidth() {
        return mMaximumWidth;
    }

    /**
     * @return The height of the Overlay Panel in dps.
     */
    public float getHeight() {
        return mHeight;
    }

    /**
     * @return Whether the Overlay Panel is fully maximized.
     */
    public boolean isMaximized() {
        return mIsMaximized;
    }

    // --------------------------------------------------------------------------------------------
    // Panel Bar states
    // --------------------------------------------------------------------------------------------
    private float mBarMarginSide;
    private float mBarHeight;
    private boolean mIsBarBorderVisible;
    private float mBarBorderHeight;

    private boolean mBarShadowVisible = false;
    private float mBarShadowOpacity = 0.f;

    private float mArrowIconOpacity;

    private float mCloseIconOpacity;
    private float mCloseIconWidth;

    /**
     * @return The side margin of the Bar.
     */
    public float getBarMarginSide() {
        return mBarMarginSide;
    }

    /**
     * @return The height of the Bar.
     */
    public float getBarHeight() {
        return mBarHeight;
    }

    /**
     * @return Whether the Bar border is visible.
     */
    public boolean isBarBorderVisible() {
        return mIsBarBorderVisible;
    }

    /**
     * @return The height of the Bar border.
     */
    public float getBarBorderHeight() {
        return mBarBorderHeight;
    }

    /**
     * @return Whether the Bar shadow is visible.
     */
    public boolean getBarShadowVisible() {
        return mBarShadowVisible;
    }

    /**
     * @return The opacity of the Bar shadow.
     */
    public float getBarShadowOpacity() {
        return mBarShadowOpacity;
    }

    /**
     * @return The opacity of the arrow icon.
     */
    public float getArrowIconOpacity() {
        return mArrowIconOpacity;
    }

    /**
     * @return The rotation of the arrow icon, in degrees.
     */
    public float getArrowIconRotation() {
        return ARROW_ICON_ROTATION;
    }

    /**
     * @return The opacity of the close icon.
     */
    public float getCloseIconOpacity() {
        return mCloseIconOpacity;
    }

    /**
     * @return The width/height of the close icon.
     */
    public float getCloseIconDimension() {
        if (mCloseIconWidth == 0) {
            mCloseIconWidth = ApiCompatibilityUtils.getDrawable(mContext.getResources(),
                    CLOSE_ICON_DRAWABLE_ID).getIntrinsicWidth() * mPxToDp;
        }
        return mCloseIconWidth;
    }

    /**
     * @return The left X coordinate of the close icon.
     */
    public float getCloseIconX() {
        if (LocalizationUtils.isLayoutRtl()) {
            return getOffsetX() + getBarMarginSide();
        } else {
            return getOffsetX() + getWidth() - getBarMarginSide() - getCloseIconDimension();
        }
    }

    // --------------------------------------------------------------------------------------------
    // Base Page states
    // --------------------------------------------------------------------------------------------

    private float mBasePageY;
    private float mBasePageBrightness;

    /**
     * @return The vertical offset of the base page.
     */
    public float getBasePageY() {
        return mBasePageY;
    }

    /**
     * @return The brightness of the base page.
     */
    public float getBasePageBrightness() {
        return mBasePageBrightness;
    }

    /**
     * @return The color to fill the base page when viewport is resized/changes orientation.
     */
    public int getBasePageBackgroundColor() {
        // TODO(pedrosimonetti): Get the color from the CVC and apply a proper brightness transform.
        // NOTE(pedrosimonetti): Assumes the background color of the base page to be white (255)
        // and applies a simple brightness transformation based on the base page value.
        int value = Math.round(255 * mBasePageBrightness);
        value = MathUtils.clamp(value, 0, 255);
        return Color.rgb(value, value, value);
    }

    // --------------------------------------------------------------------------------------------
    // Progress Bar states
    // --------------------------------------------------------------------------------------------

    private float mProgressBarOpacity;
    private boolean mIsProgressBarVisible;
    private float mProgressBarHeight;
    private int mProgressBarCompletion;

    /**
     * @return Whether the Progress Bar is visible.
     */
    public boolean isProgressBarVisible() {
        return mIsProgressBarVisible;
    }

    /**
     * @param isVisible Whether the Progress Bar should be visible.
     */
    protected void setProgressBarVisible(boolean isVisible) {
        mIsProgressBarVisible = isVisible;
    }

    /**
     * @return The Progress Bar height.
     */
    public float getProgressBarHeight() {
        return mProgressBarHeight;
    }

    /**
     * @return The Progress Bar opacity.
     */
    public float getProgressBarOpacity() {
        return mProgressBarOpacity;
    }

    /**
     * @return The completion percentage of the Progress Bar.
     */
    public int getProgressBarCompletion() {
        return mProgressBarCompletion;
    }

    /**
     * @param completion The completion percentage to be set.
     */
    protected void setProgressBarCompletion(int completion) {
        mProgressBarCompletion = completion;
    }

    // --------------------------------------------------------------------------------------------
    // Promo states
    // --------------------------------------------------------------------------------------------

    private boolean mPromoVisible = false;
    private float mPromoContentHeightPx = 0.f;
    private float mPromoHeightPx;
    private float mPromoOpacity;

    /**
     * @return Whether the promo is visible.
     */
    public boolean getPromoVisible() {
        return mPromoVisible;
    }

    /**
     * Sets the height of the promo content.
     */
    public void setPromoContentHeightPx(float heightPx) {
        mPromoContentHeightPx = heightPx;
    }

    /**
     * @return Height of the promo in dps.
     */
    public float getPromoHeight() {
        return mPromoHeightPx * mPxToDp;
    }

    /**
     * @return Height of the promo in pixels.
     */
    public float getPromoHeightPx() {
        return mPromoHeightPx;
    }

    /**
     * @return The opacity of the promo.
     */
    public float getPromoOpacity() {
        return mPromoOpacity;
    }

    /**
     * @return Y coordinate of the promo in pixels.
     */
    protected float getPromoYPx() {
        return Math.round((getOffsetY() + getBarContainerHeight()) / mPxToDp);
    }

    // ============================================================================================
    // State Handler
    // ============================================================================================

    /**
     * @return The panel's state.
     */
    public PanelState getPanelState() {
        return mPanelState;
    }

    /**
     * @return If the the panel supports an EXPANDED state rather than just PEEKING or MAXIMIZED.
     */
    public boolean supportsExpandedState() {
        return true;
    }

    /**
     * @return The {@code PanelState} that is before the |state| in the order of states.
     */
    public PanelState getPreviousPanelState(PanelState state) {
        PanelState prevState = PREVIOUS_STATES.get(state);
        if (prevState == PanelState.EXPANDED && !supportsExpandedState()) {
            prevState = PREVIOUS_STATES.get(prevState);
        }
        return prevState != null ? prevState : PanelState.UNDEFINED;
    }

    /**
     * Sets the panel's state.
     * @param state The panel state to transition to.
     * @param reason The reason for a change in the panel's state.
     */
    public void setPanelState(PanelState state, StateChangeReason reason) {
        if (state == PanelState.CLOSED) {
            mIsShowing = false;
            onClosed(reason);
        } else if (state == PanelState.EXPANDED) {
            showPromoViewAtYPosition(getPromoYPx());
        }

        // We should only set the state at the end of this method, in oder to make sure that
        // all callbacks will be fired before changing the state of the Panel. This prevents
        // some flakiness on tests since they rely on changes of state to determine when a
        // particular action has been completed.
        mPanelState = state;
    }

    /**
     * Determine if a specific {@code PanelState} is a valid state in the current environment.
     * @param state The state being evaluated.
     * @return whether the state is valid.
     */
    public boolean isValidState(PanelState state) {
        // EXPANDED is not a valid state if a panel implementation doesn't support it.
        if (!supportsExpandedState() && state == PanelState.EXPANDED) return false;
        // MAXIMIZED is not the previous state of anything, but it's a valid state.
        return PREVIOUS_STATES.values().contains(state) || state == PanelState.MAXIMIZED;
    }

    // ============================================================================================
    // Helpers
    // ============================================================================================

    /**
     * Initializes the UI state.
     */
    protected void initializeUiState() {
        mIsShowing = false;

        // Static values.
        mPxToDp = 1.f / mContext.getResources().getDisplayMetrics().density;

        mToolbarHeight = mContext.getResources().getDimension(
                getControlContainerHeightResource()) * mPxToDp;

        mBarHeightPeeking = mContext.getResources().getDimension(
                R.dimen.overlay_panel_bar_height) * mPxToDp;
        mBarHeightMaximized = mContext.getResources().getDimension(
                R.dimen.toolbar_height_no_shadow) * mPxToDp;
        mBarHeightExpanded =
                Math.round((mBarHeightPeeking + mBarHeightMaximized) / 2.f);
        mBarMarginSide = BAR_ICON_SIDE_PADDING_DP;
        mProgressBarHeight = PROGRESS_BAR_HEIGHT_DP;
        mBarBorderHeight = BAR_BORDER_HEIGHT_DP;

        // Dynamic values.
        mBarHeight = mBarHeightPeeking;
    }

    /**
     * Gets the height of the Overlay Panel in dps for a given |state|.
     *
     * @param state The state whose height will be calculated.
     * @return The height of the Overlay Panel in dps for a given |state|.
     */
    public float getPanelHeightFromState(PanelState state) {
        float tabHeight = getTabHeight();
        float panelHeight = 0;

        if (state == PanelState.UNDEFINED) {
            panelHeight = 0;
        } else if (state == PanelState.CLOSED) {
            panelHeight = 0;
        } else if (state == PanelState.PEEKED) {
            panelHeight = mBarHeightPeeking + getPeekPromoHeightPeekingPx() * mPxToDp;
        } else if (state == PanelState.EXPANDED) {
            if (isFullWidthSizePanel()) {
                panelHeight = tabHeight * EXPANDED_PANEL_HEIGHT_PERCENTAGE;
            } else {
                panelHeight = (tabHeight - mToolbarHeight)
                        * EXPANDED_PANEL_HEIGHT_PERCENTAGE;
            }
        } else if (state == PanelState.MAXIMIZED) {
            if (isFullWidthSizePanel()) {
                panelHeight = tabHeight;
            } else {
                panelHeight = tabHeight - mToolbarHeight;
            }
        }

        return panelHeight;
    }

    /**
     * @return The fraction of the distance the panel has to be to its next state before animating
     *         itself there. Default is the panel must be half of the way to the next state.
     */
    protected float getThresholdToNextState() {
        return 0.5f;
    }

    /**
     * Finds the state which has the nearest height compared to a given
     * |desiredPanelHeight|.
     *
     * @param desiredPanelHeight The height to compare to.
     * @param velocity The velocity of the swipe if applicable. The swipe is upward if less than 0.
     * @return The nearest panel state.
     */
    protected PanelState findNearestPanelStateFromHeight(float desiredPanelHeight, float velocity) {
        // If the panel was flung hard enough to make the desired height negative, it's closed.
        if (desiredPanelHeight < 0) return PanelState.CLOSED;

        // First, find the two states that the desired panel height is between.
        PanelState nextState = PanelState.values()[0];
        PanelState prevState = nextState;
        for (PanelState state : PanelState.values()) {
            if (!isValidState(state)) {
                continue;
            }
            prevState = nextState;
            nextState = state;
            // The values in PanelState are ascending, they should be kept that way in order for
            // this to work.
            if (desiredPanelHeight >= getPanelHeightFromState(prevState)
                    && desiredPanelHeight < getPanelHeightFromState(nextState)) {
                break;
            }
        }

        // If the desired height is close enough to a certain state, depending on the direction of
        // the velocity, move to that state.
        float lowerBound = getPanelHeightFromState(prevState);
        float distance = getPanelHeightFromState(nextState) - lowerBound;
        float thresholdToNextState = velocity < 0.0f
                ? getThresholdToNextState() : 1.0f - getThresholdToNextState();
        if ((desiredPanelHeight - lowerBound) / distance > thresholdToNextState) {
            return nextState;
        } else {
            return prevState;
        }
    }

    /**
     * Sets the last panel height within the limits allowable by our UI.
     *
     * @param height The height of the panel in dps.
     */
    protected void setClampedPanelHeight(float height) {
        final float clampedHeight = MathUtils.clamp(height,
                getPanelHeightFromState(PanelState.MAXIMIZED),
                getPanelHeightFromState(PanelState.PEEKED));
        setPanelHeight(clampedHeight);
    }

    /**
     * Sets the panel height.
     *
     * @param height The height of the panel in dps.
     */
    protected void setPanelHeight(float height) {
        // As soon as we resize the Panel to a different height than the expanded one,
        // then we should hide the Promo View once the snapshot will be shown in its place.
        if (height != getPanelHeightFromState(PanelState.EXPANDED)) {
            hidePromoView();
        }

        updatePanelForHeight(height);
    }

    /**
     * @param state The Panel state.
     * @return Whether the Panel height matches the one from the given state.
     */
    protected boolean doesPanelHeightMatchState(PanelState state) {
        return state == getPanelState() && getHeight() == getPanelHeightFromState(state);
    }

    // ============================================================================================
    // UI Update Handling
    // ============================================================================================

    /**
     * Updates the UI state for a given |height|.
     *
     * @param height The Overlay Panel height.
     */
    private void updatePanelForHeight(float height) {
        PanelState endState = findLargestPanelStateFromHeight(height);
        PanelState startState = getPreviousPanelState(endState);
        float percentage = getStateCompletion(height, startState, endState);

        updatePanelSize(height, endState, percentage);

        if (endState == PanelState.CLOSED || endState == PanelState.PEEKED) {
            updatePanelForCloseOrPeek(percentage);
        } else if (endState == PanelState.EXPANDED) {
            updatePanelForExpansion(percentage);
        } else if (endState == PanelState.MAXIMIZED) {
            updatePanelForMaximization(percentage);
        }
    }

    /**
     * Updates the Panel size information.
     *
     * @param height The Overlay Panel height.
     * @param endState The final state of transition being executed.
     * @param percentage The completion percentage of the transition.
     */
    private void updatePanelSize(float height, PanelState endState, float percentage) {
        mHeight = height;
        mOffsetX = calculateOverlayPanelX();
        mOffsetY = calculateOverlayPanelY();
        mIsMaximized = height == getPanelHeightFromState(PanelState.MAXIMIZED);
    }

    /**
     * Finds the largest Panel state which is being transitioned to/from.
     * Whenever the Panel is in between states, let's say, when resizing the
     * Panel from its peeked to expanded state, we need to know those two states
     * in order to calculate how closely we are from one of them. This method
     * will always return the nearest state with the largest height, and
     * together with the state preceding it, it's possible to calculate how far
     * the Panel is from them.
     *
     * @param panelHeight The height to compare to.
     * @return The panel state which is being transitioned to/from.
     */
    private PanelState findLargestPanelStateFromHeight(float panelHeight) {
        PanelState stateFound = PanelState.CLOSED;

        // Iterate over all states and find the largest one which is being
        // transitioned to/from.
        for (PanelState state : PanelState.values()) {
            if (!isValidState(state)) {
                continue;
            }
            if (panelHeight <= getPanelHeightFromState(state)) {
                stateFound = state;
                break;
            }
        }

        return stateFound;
    }

    /**
     * Gets the state completion percentage, taking into consideration the |height| of the Overlay
     * Panel, and the initial and final states. A completion of 0 means the Panel is in the initial
     * state and a completion of 1 means the Panel is in the final state.
     *
     * @param height The height of the Overlay Panel.
     * @param startState The initial state of the Panel.
     * @param endState The final state of the Panel.
     * @return The completion percentage.
     */
    private float getStateCompletion(float height, PanelState startState, PanelState endState) {
        float startSize = getPanelHeightFromState(startState);
        float endSize = getPanelHeightFromState(endState);
        float percentage =
                // NOTE(pedrosimonetti): Handle special case from PanelState.UNDEFINED
                // to PanelState.CLOSED, where both have a height of zero. Returning
                // zero here means the Panel will be reset to its CLOSED state.
                startSize == 0.f && endSize == 0.f ? 0.f
                        : (height - startSize) / (endSize - startSize);

        return percentage;
    }

    /**
     * Updates the UI state for the closed to peeked transition (and vice
     * versa), according to a completion |percentage|.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForCloseOrPeek(float percentage) {
        // Update the opt out promo.
        updatePromoVisibility(1.f);

        // Base page offset.
        mBasePageY = 0.f;

        // Base page brightness.
        mBasePageBrightness = BASE_PAGE_BRIGHTNESS_STATE_PEEKED;

        // Bar height.
        mBarHeight = mBarHeightPeeking;

        // Bar border.
        mIsBarBorderVisible = false;

        // Arrow Icon.
        mArrowIconOpacity = ARROW_ICON_OPACITY_STATE_PEEKED;

        // Close icon opacity.
        mCloseIconOpacity = CLOSE_ICON_OPACITY_STATE_PEEKED;

        // Progress Bar.
        mProgressBarOpacity = 0.f;

        // Update the Bar Shadow.
        updateBarShadow();
    }

    /**
     * Updates the UI state for the peeked to expanded transition (and vice
     * versa), according to a completion |percentage|.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForExpansion(float percentage) {
        // Update the opt out promo.
        updatePromoVisibility(1.f);

        // Base page offset.
        float baseBaseY = MathUtils.interpolate(
                0.f,
                getBasePageTargetY(),
                percentage);
        mBasePageY = baseBaseY;

        // Base page brightness.
        float brightness = MathUtils.interpolate(
                BASE_PAGE_BRIGHTNESS_STATE_PEEKED,
                BASE_PAGE_BRIGHTNESS_STATE_EXPANDED,
                percentage);
        mBasePageBrightness = brightness;

        // Bar height.
        float barHeight = Math.round(MathUtils.interpolate(
                mBarHeightPeeking,
                getBarHeightExpanded(),
                percentage));
        mBarHeight = barHeight;

        // Bar border.
        mIsBarBorderVisible = true;

        // Determine fading element opacities. The arrow icon needs to finish fading out before
        // the close icon starts fading in. Any other elements fading in or fading out should use
        // the same percentage.
        float fadingOutPercentage = Math.min(percentage, .5f) / .5f;
        float fadingInPercentage = Math.max(percentage - .5f, 0.f) / .5f;

        // Arrow Icon.
        mArrowIconOpacity = MathUtils.interpolate(
                ARROW_ICON_OPACITY_STATE_PEEKED,
                ARROW_ICON_OPACITY_STATE_EXPANDED,
                fadingOutPercentage);

        // Close Icon.
        mCloseIconOpacity = MathUtils.interpolate(
                CLOSE_ICON_OPACITY_STATE_PEEKED,
                CLOSE_ICON_OPACITY_STATE_EXPANDED,
                fadingInPercentage);

        // Progress Bar.
        float peekedHeight = getPanelHeightFromState(PanelState.PEEKED);
        float threshold = PROGRESS_BAR_VISIBILITY_THRESHOLD_DP / mPxToDp;
        float diff = Math.min(mHeight - peekedHeight, threshold);
        // Fades the Progress Bar the closer it gets to the bottom of the
        // screen.
        float progressBarOpacity = MathUtils.interpolate(0.f, 1.f, diff / threshold);
        mProgressBarOpacity = progressBarOpacity;

        // Update the Bar Shadow.
        updateBarShadow();
    }

    /**
     * Updates the UI state for the expanded to maximized transition (and vice
     * versa), according to a completion |percentage|.
     *
     * @param percentage The completion percentage.
     */
    protected void updatePanelForMaximization(float percentage) {
        // Update the opt out promo.
        updatePromoVisibility(1.f - percentage);

        // Base page offset.
        float startTargetY = supportsExpandedState() ? getBasePageTargetY() : 0.0f;
        mBasePageY = MathUtils.interpolate(
                startTargetY,
                getBasePageTargetY(),
                percentage);

        // Base page brightness.
        float startBrightness = supportsExpandedState()
                ? BASE_PAGE_BRIGHTNESS_STATE_EXPANDED : BASE_PAGE_BRIGHTNESS_STATE_PEEKED;
        mBasePageBrightness = MathUtils.interpolate(
                startBrightness,
                BASE_PAGE_BRIGHTNESS_STATE_MAXIMIZED,
                percentage);

        // Bar height.
        float startBarHeight = supportsExpandedState()
                ? getBarHeightExpanded() : getBarHeightPeeking();
        mBarHeight = Math.round(MathUtils.interpolate(
                startBarHeight,
                getBarHeightMaximized(),
                percentage));

        // Bar border.
        mIsBarBorderVisible = true;

        // Arrow Icon.
        mArrowIconOpacity = ARROW_ICON_OPACITY_STATE_MAXIMIZED;

        // Close Icon.
        mCloseIconOpacity = CLOSE_ICON_OPACITY_STATE_MAXIMIZED;

        // Progress Bar.
        mProgressBarOpacity = 1.f;

        // Update the Bar Shadow.
        updateBarShadow();
    }

    private float getBarHeightExpanded() {
        if (isFullWidthSizePanel()) {
            return mBarHeightExpanded;
        } else {
            return mBarHeightPeeking;
        }
    }

    private float getBarHeightMaximized() {
        if (isFullWidthSizePanel()) {
            return mBarHeightMaximized;
        } else {
            return mBarHeightPeeking;
        }
    }

    /**
     * @return The peeking height of the panel's bar in dp.
     */
    protected float getBarHeightPeeking() {
        return mBarHeightPeeking;
    }

    /**
     * @return Whether the Panel Promo is visible.
     */
    protected boolean isPromoVisible() {
        return false;
    }

    /**
     * Updates the UI state for Opt Out Promo.
     *
     * @param percentage The visibility percentage of the Promo. A visibility of 0 means the
     * Promo is not visible. A visibility of 1 means the Promo is fully visible. And
     * visibility between 0 and 1 means the Promo is partially visible.
     */
    private void updatePromoVisibility(float percentage) {
        if (isPromoVisible()) {
            createPromoView();

            mPromoVisible = true;

            mPromoHeightPx = Math.round(MathUtils.clamp(percentage * mPromoContentHeightPx,
                    0.f, mPromoContentHeightPx));
            mPromoOpacity = percentage;
        } else {
            mPromoVisible = false;
            mPromoHeightPx = 0.f;
            mPromoOpacity = 0.f;
        }
    }

    /**
     * Updates the UI state for Bar Shadow.
     */
    public void updateBarShadow() {
        float barShadowHeightPx = 9.f / mPxToDp;
        if (mPromoVisible && mPromoHeightPx > 0.f) {
            mBarShadowVisible = true;
            float threshold = 2 * barShadowHeightPx;
            mBarShadowOpacity = mPromoHeightPx > barShadowHeightPx ? 1.f
                    : MathUtils.interpolate(0.f, 1.f, mPromoHeightPx / threshold);
        } else {
            mBarShadowVisible = false;
            mBarShadowOpacity = 0.f;
        }
    }

    // ============================================================================================
    // Base Page Offset
    // ============================================================================================

    /**
     * Updates the coordinate of the existing selection.
     * @param y The y coordinate of the selection in pixels.
     */
    protected void updateBasePageSelectionYPx(float y) {
        mBasePageSelectionYPx = y;
        updateBasePageTargetY();
    }

    /**
     * Updates the target offset of the Base Page in order to keep the selection in view
     * after expanding the Panel.
     */
    protected void updateBasePageTargetY() {
        mBasePageTargetY = calculateBasePageTargetY(PanelState.EXPANDED);
    }

    /**
     * Calculates the target offset of the Base Page in order to keep the selection in view
     * after expanding the Panel to the given |expandedState|.
     *
     * @param expandedState
     * @return The target offset Y.
     */
    protected float calculateBasePageTargetY(PanelState expandedState) {
        // Only a fullscreen wide Panel should offset the base page. A small panel should
        // always return zero to ensure the Base Page remains in the same position.
        if (!isFullWidthSizePanel()) return 0.f;

        // Convert from px to dp.
        final float selectionY = mBasePageSelectionYPx * mPxToDp;

        // Calculate the exact height of the expanded Panel without taking into
        // consideration the height of the shadow (what is returned by the
        // getPanelFromHeight method). We need the measurement of the portion
        // of the Panel that occludes the page.
        final float expandedHeight = getPanelHeightFromState(expandedState);

        // Calculate the offset to center the selection on the available area.
        final float tabHeight = getTabHeight();
        final float availableHeight = tabHeight - expandedHeight;
        float offset = -selectionY + availableHeight / 2;

        // Make sure offset is negative to prevent Base Page from moving down,
        // because there's nothing to render above the Page.
        offset = Math.min(offset, 0.f);
        // If visible, the Toolbar will be hidden. Therefore, we need to adjust
        // the offset to account for this difference.
        offset -= (mToolbarHeight - getTopControlsOffsetDp());
        // Make sure the offset is not greater than the expanded height, because
        // there's nothing to render below the Page.
        offset = Math.max(offset, -expandedHeight);

        return offset;
    }

    /**
     * @return The Y coordinate to apply to the Base Page in order to keep the selection
     *         in view when the Overlay Panel is in EXPANDED state.
     */
    public float getBasePageTargetY() {
        return mBasePageTargetY;
    }

    // ============================================================================================
    // Resource Loader
    // ============================================================================================

    protected ViewGroup mContainerView;
    protected DynamicResourceLoader mResourceLoader;

    /**
     * @param resourceLoader The {@link DynamicResourceLoader} to register and unregister the view.
     */
    public void setDynamicResourceLoader(DynamicResourceLoader resourceLoader) {
        mResourceLoader = resourceLoader;

        if (mPromoView != null) {
            mResourceLoader.registerResource(R.id.contextual_search_opt_out_promo,
                    mPromoView.getResourceAdapter());
        }
    }

    /**
     * Sets the container ViewGroup to which the auxiliary Views will be attached to.
     *
     * @param container The {@link ViewGroup} container.
     */
    public void setContainerView(ViewGroup container) {
        mContainerView = container;
    }

    // ============================================================================================
    // Promo Host
    // ============================================================================================

    @Override
    public void onPromoPreferenceClick() {
        new Handler().post(new Runnable() {
            @Override
            public void run() {
                PreferencesLauncher.launchSettingsPage(mContext,
                        ContextualSearchPreferenceFragment.class.getName());
            }
        });
    }

    /**
     * @param enabled Whether the preference should be enabled.
     */
    public void setPreferenceState(boolean enabled) {
    }

    @Override
    public void onPromoButtonClick(boolean accepted) {
        if (accepted) {
            animatePromoAcceptance();
        } else {
            hidePromoView();
            // NOTE(pedrosimonetti): If the user has opted out of Contextual Search, we should set
            // the preference right away because the preference state controls whether the promo
            // will be visible, and we want to hide the promo immediately when the user opts out.
            setPreferenceState(false);
            closePanel(StateChangeReason.OPTOUT, true);
        }
    }

    // ============================================================================================
    // Opt Out Promo View
    // ============================================================================================

    // TODO(pedrosimonetti): Consider maybe adding a 9.patch to avoid the hacky nested layouts in
    // order to have the transparent gap at the top of the Promo View.

    /**
     * The {@link ContextualSearchOptOutPromo} instance.
     */
    private ContextualSearchOptOutPromo mPromoView;

    /**
     * Whether the Search Promo View is visible.
     */
    private boolean mIsSearchPromoViewVisible = false;

    /**
     * Creates the Search Promo View.
     */
    protected void createPromoView() {
        if (isPromoVisible() && mPromoView == null) {
            assert mContainerView != null;

            // TODO(pedrosimonetti): Refactor promo code to use ViewResourceInflater.
            LayoutInflater.from(mContext).inflate(
                    R.layout.contextual_search_opt_out_promo, mContainerView);
            mPromoView = (ContextualSearchOptOutPromo)
                    mContainerView.findViewById(R.id.contextual_search_opt_out_promo);

            final int maximumWidth = getMaximumWidthPx();

            // Adjust size for small Panel.
            if (!isFullWidthSizePanel()) {
                mPromoView.getLayoutParams().width = maximumWidth;
                mPromoView.requestLayout();
            }

            if (mResourceLoader != null) {
                mResourceLoader.registerResource(R.id.contextual_search_opt_out_promo,
                        mPromoView.getResourceAdapter());
            }

            mPromoView.setPromoHost(this);
            setPromoContentHeightPx(mPromoView.getHeightForGivenWidth(maximumWidth));
        }

        assert mPromoView != null;
    }

    /**
     * Destroys the Search Promo View.
     */
    protected void destroyPromoView() {
        if (mPromoView != null) {
            mContainerView.removeView(mPromoView);
            mPromoView = null;
            if (mResourceLoader != null) {
                mResourceLoader.unregisterResource(R.id.contextual_search_opt_out_promo);
            }
        }
    }

    /**
     * Displays the Search Promo View at the given Y position.
     *
     * @param y The Y position.
     */
    public void showPromoViewAtYPosition(float y) {
        if (mPromoView == null || !isPromoVisible()) return;

        float offsetX = getOffsetX() / mPxToDp;
        if (LocalizationUtils.isLayoutRtl()) {
            offsetX = -offsetX;
        }

        mPromoView.setTranslationX(offsetX);
        mPromoView.setTranslationY(y);
        mPromoView.setVisibility(View.VISIBLE);

        // NOTE(pedrosimonetti): We need to call requestLayout, otherwise
        // the Promo View will not become visible.
        mPromoView.requestLayout();

        mIsSearchPromoViewVisible = true;
    }

    /**
     * Hides the Search Promo View.
     */
    public void hidePromoView() {
        if (mPromoView == null
                || !mIsSearchPromoViewVisible
                || !isPromoVisible()) {
            return;
        }

        mPromoView.setVisibility(View.INVISIBLE);

        mIsSearchPromoViewVisible = false;
    }

    /**
     * Updates the UI state for Opt In Promo animation.
     *
     * @param percentage The visibility percentage of the Promo.
     */
    protected void setPromoVisibilityForOptInAnimation(float percentage) {
        updatePromoVisibility(percentage);
        updateBarShadow();
    }
}


text:
/* This is a compiled file, to make changes persist, consider editing under the templates directory */
.pace {
  -webkit-pointer-events: none;
  pointer-events: none;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}

.pace-inactive {
  display: none;
}

.pace .pace-progress {
  background-color: rgba(238, 49, 72, 0.19999999999999996);
  position: fixed;
  z-index: -1;
  top: 0;
  right: 100%;
  bottom: 0;
  width: 100%;
}

redpajama_set_name: RedPajamaGithub


text:
List rows from Google Sheets in Mozaïk dashboard. The extension can be handy for various
use cases where data is maintained in Google Sheets:

- List events (including dates etc)
- Show game results
- Show event participants, company vacation listing
- Etc.

![preview](https://raw.githubusercontent.com/SC5/mozaik-ext-sheets/master/preview.png)

- The widget tries to provide a generic but easy-to-use way to show content in dashboard.
- You can also utilise the Sheets functions as the outcome is shown in widget
- Use format functions to format some cell data
- Use filter function to leave out some results

## Setup

- Install module

    ```bash
    npm install --save mozaik-ext-sheets
    ```

- Create a project and service account in Google Developer Console
- Enable API: Drive API
- Collect service email and .p12 file
- Convert .p12 file into .PEM
- Configure service key and .PEM file into dashboard ``config.js`` file and
  environment variables / ``.env`` file:

    ```javascript
    api: {
      sheets: {
        googleServiceEmail: process.env.GOOGLE_SERVICE_EMAIL,
        googleServiceKeypath: process.env.GOOGLE_SERVICE_KEYPATH
      }
    }
    ```

- Done.

## Widget: List

Show cell contents in a listing. Use dashboard theme to customize the outcome to match with your requirements.

### parameters

key           | required | description
--------------|----------|---------------
`documentId`  | yes      | *Sheets document id, taken form web UI*
`sheetNo`     | no       | *Sheet order number, starting from 0. Defaults to first: `0`*
`range`       | no       | *Range from where to list data. Example: `A2:C10` or `A2:`. Defaults to full sheet*
`style`       | no       | *Show results as bullet list or table-like. Valid values: `list` (default) or `table`*
`styles`      | no       | *Inline style defintions in React supported style format. Sub-elements: list, listItem`*
`fields`      | no       | *Columns to list, using advanced formatting*
`random`      | no       | *Pick and show random number entries, defined by the value. Zero means no random is used (Default). Example: `random: 1`*
`fontSize`    | no       | *Use default (``), fixed  (`12px`) or auto scaled fontsize (`auto`)*
`format`      | no       | *Custom formating functions in object, where key is name of the formatter and used like {COLUMNLETTER!formatter}. See usage for examples*
`filter`      | no       | *Filter some rows out of the outcome by implementing the function. See usage for examples*

### usage

Imaginary example of Google Sheet document cells:

```
    |      A     |       B       |       C       |  D  |
 1  | 2015-01-28 | React.js Conf | Facebook HQ   |     |
 2  | 2015-07-02 | ReactEurope   | Paris, France |     |
```

One widget in dashboard config:

```javascript
// widget in config.js
{
  type: 'sheets.list',
  // You can find the documentId in sheet URL
  documentId: 'abasdfsdfafsd123123',
  range: 'A1:D10',
  // Use either 'list' or 'table'
  // Table shows every other row with different bg color
  style: 'list',
  fontSize: 'auto', // Use default, fixed or auto-scaled font size
  // Customize the list or list item styles
  styles: {
    list: {
      listStyle: 'none'
    },
    listItem: {
      textAlign: 'center'
    }
  },
  // Values (cells) to show on each row. Use one or multiple column letters:
  // Uses https://www.npmjs.com/package/string-format for formatting
  // NOTE: The both `['{A} {B} {C}']` and `['{A}', '{B}', '{C}']` work, but
  // latter makes it possible to style each field / show the values in different
  // cells in table format
  fields: [
    'Event date: {A!date}',
    '{B!uppercase}',
    '{C} {D}'
  ],
  // Show two random entries (after optional filtering)
  // Handy for picking random quote of the day etc.
  random: 2,
  // Custom formatter functions, name must match with the usage: !method
  // NOTE: Call method.toString() to every function!
  // NOTE: `moment` is available
  format: {
    // !date formatter
    date: function(s){
      return new moment(s).format('YYYY-MM-DD');
    }.toString(),
    // !uppercase formatter
    uppercase: function(s) {
      return s.toUpperCase();
    }.toString()
  },
  // Custom function to filter some results rows
  // If defined, each row is processed through it.
  // Return `false` if you want to filter the row out and `true`
  // for inclusion.
  // NOTE: Only one variable is passed, containing all columns from current row
  // NOTE: Variable `columns` does not contain the columns out of the range
  // NOTE: Call method.toString() to every function!
  filter: function(columns, index){
    // Filter out first two lines
    if (index < 2) {
      return false;
    }

    // Filter out the results that are in past
    var eventStart = moment(columns.B, ['YYYY-MM-DD']);
    if (eventStart.isBefore(new moment())) {
      return false;
    }

    // Otherwise show the fields from that row
    return true;
  }.toString(),
  columns: 1, rows: 2,
  x: 0, y: 0
}
```

## Changelog

### Release 0.6.0

- Added support showing `random` number (0-n) of entries
- Added support for styling elements with widget arguments

### Release 0.5.0

- Added support for table -styled listing

### Release 0.4.1

- Fixed missing module: src/client

### Release 0.4.0

- Replaced Gulp + Karma with Ava testing
- Started using Eslint
- Fixed dependencies
- Fixed format support

### Release 0.3.0

- Updated support to Mozaik 1.4

### Release 0.2.0

- Added support for filtering

### Release 0.1.0

- Initial release

## License

Module is MIT -licensed

## Credit

Module is backed by: [SC5](http://sc5.io)


text:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.Runtime.Serialization;

namespace SissOnlineWithEntityFrameworkOnSqlServer
{
    [DataContract(IsReference = true)]
    [KnownType(typeof(TB_AC_VACINA_DOSE))]
    public partial class TB_AC_VACINA_DOSE_REGRA: IObjectWithChangeTracker, INotifyPropertyChanged
    {
        #region Primitive Properties

        [DataMember]
        public long CD_VACINA_DOSE_REGRA
        {
            get { return _cD_VACINA_DOSE_REGRA; }
            set
            {
                if (_cD_VACINA_DOSE_REGRA != value)
                {
                    if (ChangeTracker.ChangeTrackingEnabled && ChangeTracker.State != ObjectState.Added)
                    {
                        throw new InvalidOperationException("The property 'CD_VACINA_DOSE_REGRA' is part of the object's key and cannot be changed. Changes to key properties can only be made when the object is not being tracked or is in the Added state.");
                    }
                    _cD_VACINA_DOSE_REGRA = value;
                    OnPropertyChanged("CD_VACINA_DOSE_REGRA");
                }
            }
        }
        private long _cD_VACINA_DOSE_REGRA;

        [DataMember]
        public byte IC_TIPO_REGRA
        {
            get { return _iC_TIPO_REGRA; }
            set
            {
                if (_iC_TIPO_REGRA != value)
                {
                    _iC_TIPO_REGRA = value;
                    OnPropertyChanged("IC_TIPO_REGRA");
                }
            }
        }
        private byte _iC_TIPO_REGRA;

        [DataMember]
        public Nullable<byte> IC_TIPO_MENSAGEM
        {
            get { return _iC_TIPO_MENSAGEM; }
            set
            {
                if (_iC_TIPO_MENSAGEM != value)
                {
                    _iC_TIPO_MENSAGEM = value;
                    OnPropertyChanged("IC_TIPO_MENSAGEM");
                }
            }
        }
        private Nullable<byte> _iC_TIPO_MENSAGEM;

        [DataMember]
        public string DS_MENSAGEM
        {
            get { return _dS_MENSAGEM; }
            set
            {
                if (_dS_MENSAGEM != value)
                {
                    _dS_MENSAGEM = value;
                    OnPropertyChanged("DS_MENSAGEM");
                }
            }
        }
        private string _dS_MENSAGEM;

        [DataMember]
        public byte IC_ATIVO
        {
            get { return _iC_ATIVO; }
            set
            {
                if (_iC_ATIVO != value)
                {
                    _iC_ATIVO = value;
                    OnPropertyChanged("IC_ATIVO");
                }
            }
        }
        private byte _iC_ATIVO;

        [DataMember]
        public System.DateTime DT_INCLUSAO
        {
            get { return _dT_INCLUSAO; }
            set
            {
                if (_dT_INCLUSAO != value)
                {
                    _dT_INCLUSAO = value;
                    OnPropertyChanged("DT_INCLUSAO");
                }
            }
        }
        private System.DateTime _dT_INCLUSAO;

        [DataMember]
        public decimal CD_USUARIO_INCLUSAO
        {
            get { return _cD_USUARIO_INCLUSAO; }
            set
            {
                if (_cD_USUARIO_INCLUSAO != value)
                {
                    _cD_USUARIO_INCLUSAO = value;
                    OnPropertyChanged("CD_USUARIO_INCLUSAO");
                }
            }
        }
        private decimal _cD_USUARIO_INCLUSAO;

        [DataMember]
        public Nullable<short> CD_VACINA
        {
            get { return _cD_VACINA; }
            set
            {
                if (_cD_VACINA != value)
                {
                    ChangeTracker.RecordOriginalValue("CD_VACINA", _cD_VACINA);
                    if (!IsDeserializing)
                    {
                        if (TB_AC_VACINA_DOSE != null && TB_AC_VACINA_DOSE.CD_VACINA != value)
                        {
                            var previousValue = _tB_AC_VACINA_DOSE;
                            _tB_AC_VACINA_DOSE = null;
                            FixupTB_AC_VACINA_DOSE(previousValue, skipKeys: true);
                            OnNavigationPropertyChanged("TB_AC_VACINA_DOSE");
                        }
                    }
                    _cD_VACINA = value;
                    OnPropertyChanged("CD_VACINA");
                }
            }
        }
        private Nullable<short> _cD_VACINA;

        [DataMember]
        public Nullable<byte> CD_DOSE_VACINA
        {
            get { return _cD_DOSE_VACINA; }
            set
            {
                if (_cD_DOSE_VACINA != value)
                {
                    ChangeTracker.RecordOriginalValue("CD_DOSE_VACINA", _cD_DOSE_VACINA);
                    if (!IsDeserializing)
                    {
                        if (TB_AC_VACINA_DOSE != null && TB_AC_VACINA_DOSE.CD_DOSE_VACINA != value)
                        {
                            var previousValue = _tB_AC_VACINA_DOSE;
                            _tB_AC_VACINA_DOSE = null;
                            FixupTB_AC_VACINA_DOSE(previousValue, skipKeys: true);
                            OnNavigationPropertyChanged("TB_AC_VACINA_DOSE");
                        }
                    }
                    _cD_DOSE_VACINA = value;
                    OnPropertyChanged("CD_DOSE_VACINA");
                }
            }
        }
        private Nullable<byte> _cD_DOSE_VACINA;

        [DataMember]
        public Nullable<short> CD_VACINA_REGRA
        {
            get { return _cD_VACINA_REGRA; }
            set
            {
                if (_cD_VACINA_REGRA != value)
                {
                    ChangeTracker.RecordOriginalValue("CD_VACINA_REGRA", _cD_VACINA_REGRA);
                    if (!IsDeserializing)
                    {
                        if (TB_AC_VACINA_DOSE1 != null && TB_AC_VACINA_DOSE1.CD_VACINA != value)
                        {
                            var previousValue = _tB_AC_VACINA_DOSE1;
                            _tB_AC_VACINA_DOSE1 = null;
                            FixupTB_AC_VACINA_DOSE1(previousValue, skipKeys: true);
                            OnNavigationPropertyChanged("TB_AC_VACINA_DOSE1");
                        }
                    }
                    _cD_VACINA_REGRA = value;
                    OnPropertyChanged("CD_VACINA_REGRA");
                }
            }
        }
        private Nullable<short> _cD_VACINA_REGRA;

        [DataMember]
        public Nullable<byte> CD_DOSE_VACINA_REGRA
        {
            get { return _cD_DOSE_VACINA_REGRA; }
            set
            {
                if (_cD_DOSE_VACINA_REGRA != value)
                {
                    ChangeTracker.RecordOriginalValue("CD_DOSE_VACINA_REGRA", _cD_DOSE_VACINA_REGRA);
                    if (!IsDeserializing)
                    {
                        if (TB_AC_VACINA_DOSE1 != null && TB_AC_VACINA_DOSE1.CD_DOSE_VACINA != value)
                        {
                            var previousValue = _tB_AC_VACINA_DOSE1;
                            _tB_AC_VACINA_DOSE1 = null;
                            FixupTB_AC_VACINA_DOSE1(previousValue, skipKeys: true);
                            OnNavigationPropertyChanged("TB_AC_VACINA_DOSE1");
                        }
                    }
                    _cD_DOSE_VACINA_REGRA = value;
                    OnPropertyChanged("CD_DOSE_VACINA_REGRA");
                }
            }
        }
        private Nullable<byte> _cD_DOSE_VACINA_REGRA;

        [DataMember]
        public Nullable<int> VL_IDADE_INICIAL
        {
            get { return _vL_IDADE_INICIAL; }
            set
            {
                if (_vL_IDADE_INICIAL != value)
                {
                    _vL_IDADE_INICIAL = value;
                    OnPropertyChanged("VL_IDADE_INICIAL");
                }
            }
        }
        private Nullable<int> _vL_IDADE_INICIAL;

        [DataMember]
        public Nullable<int> VL_IDADE_FINAL
        {
            get { return _vL_IDADE_FINAL; }
            set
            {
                if (_vL_IDADE_FINAL != value)
                {
                    _vL_IDADE_FINAL = value;
                    OnPropertyChanged("VL_IDADE_FINAL");
                }
            }
        }
        private Nullable<int> _vL_IDADE_FINAL;

        [DataMember]
        public Nullable<int> VL_INTERVALO
        {
            get { return _vL_INTERVALO; }
            set
            {
                if (_vL_INTERVALO != value)
                {
                    _vL_INTERVALO = value;
                    OnPropertyChanged("VL_INTERVALO");
                }
            }
        }
        private Nullable<int> _vL_INTERVALO;

        [DataMember]
        public Nullable<System.DateTime> DT_ALTERACAO
        {
            get { return _dT_ALTERACAO; }
            set
            {
                if (_dT_ALTERACAO != value)
                {
                    _dT_ALTERACAO = value;
                    OnPropertyChanged("DT_ALTERACAO");
                }
            }
        }
        private Nullable<System.DateTime> _dT_ALTERACAO;

        [DataMember]
        public Nullable<decimal> CD_USUARIO_ALTERACAO
        {
            get { return _cD_USUARIO_ALTERACAO; }
            set
            {
                if (_cD_USUARIO_ALTERACAO != value)
                {
                    _cD_USUARIO_ALTERACAO = value;
                    OnPropertyChanged("CD_USUARIO_ALTERACAO");
                }
            }
        }
        private Nullable<decimal> _cD_USUARIO_ALTERACAO;

        #endregion
        #region Navigation Properties

        [DataMember]
        public TB_AC_VACINA_DOSE TB_AC_VACINA_DOSE
        {
            get { return _tB_AC_VACINA_DOSE; }
            set
            {
                if (!ReferenceEquals(_tB_AC_VACINA_DOSE, value))
                {
                    var previousValue = _tB_AC_VACINA_DOSE;
                    _tB_AC_VACINA_DOSE = value;
                    FixupTB_AC_VACINA_DOSE(previousValue);
                    OnNavigationPropertyChanged("TB_AC_VACINA_DOSE");
                }
            }
        }
        private TB_AC_VACINA_DOSE _tB_AC_VACINA_DOSE;

        [DataMember]
        public TB_AC_VACINA_DOSE TB_AC_VACINA_DOSE1
        {
            get { return _tB_AC_VACINA_DOSE1; }
            set
            {
                if (!ReferenceEquals(_tB_AC_VACINA_DOSE1, value))
                {
                    var previousValue = _tB_AC_VACINA_DOSE1;
                    _tB_AC_VACINA_DOSE1 = value;
                    FixupTB_AC_VACINA_DOSE1(previousValue);
                    OnNavigationPropertyChanged("TB_AC_VACINA_DOSE1");
                }
            }
        }
        private TB_AC_VACINA_DOSE _tB_AC_VACINA_DOSE1;

        #endregion
        #region ChangeTracking

        protected virtual void OnPropertyChanged(String propertyName)
        {
            if (ChangeTracker.State != ObjectState.Added && ChangeTracker.State != ObjectState.Deleted)
            {
                ChangeTracker.State = ObjectState.Modified;
            }
            if (_propertyChanged != null)
            {
                _propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        protected virtual void OnNavigationPropertyChanged(String propertyName)
        {
            if (_propertyChanged != null)
            {
                _propertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged{ add { _propertyChanged += value; } remove { _propertyChanged -= value; } }
        private event PropertyChangedEventHandler _propertyChanged;
        private ObjectChangeTracker _changeTracker;

        [DataMember]
        public ObjectChangeTracker ChangeTracker
        {
            get
            {
                if (_changeTracker == null)
                {
                    _changeTracker = new ObjectChangeTracker();
                    _changeTracker.ObjectStateChanging += HandleObjectStateChanging;
                }
                return _changeTracker;
            }
            set
            {
                if(_changeTracker != null)
                {
                    _changeTracker.ObjectStateChanging -= HandleObjectStateChanging;
                }
                _changeTracker = value;
                if(_changeTracker != null)
                {
                    _changeTracker.ObjectStateChanging += HandleObjectStateChanging;
                }
            }
        }

        private void HandleObjectStateChanging(object sender, ObjectStateChangingEventArgs e)
        {
            if (e.NewState == ObjectState.Deleted)
            {
                ClearNavigationProperties();
            }
        }

        protected bool IsDeserializing { get; private set; }

        [OnDeserializing]
        public void OnDeserializingMethod(StreamingContext context)
        {
            IsDeserializing = true;
        }

        [OnDeserialized]
        public void OnDeserializedMethod(StreamingContext context)
        {
            IsDeserializing = false;
            ChangeTracker.ChangeTrackingEnabled = true;
        }

        protected virtual void ClearNavigationProperties()
        {
            TB_AC_VACINA_DOSE = null;
            TB_AC_VACINA_DOSE1 = null;
        }

        #endregion
        #region Association Fixup

        private void FixupTB_AC_VACINA_DOSE(TB_AC_VACINA_DOSE previousValue, bool skipKeys = false)
        {
            if (IsDeserializing)
            {
                return;
            }

            if (previousValue != null && previousValue.TB_AC_VACINA_DOSE_REGRA.Contains(this))
            {
                previousValue.TB_AC_VACINA_DOSE_REGRA.Remove(this);
            }

            if (TB_AC_VACINA_DOSE != null)
            {
                if (!TB_AC_VACINA_DOSE.TB_AC_VACINA_DOSE_REGRA.Contains(this))
                {
                    TB_AC_VACINA_DOSE.TB_AC_VACINA_DOSE_REGRA.Add(this);
                }

                CD_VACINA = TB_AC_VACINA_DOSE.CD_VACINA;
                CD_DOSE_VACINA = TB_AC_VACINA_DOSE.CD_DOSE_VACINA;
            }
            else if (!skipKeys)
            {
                CD_VACINA = null;
                CD_DOSE_VACINA = null;
            }

            if (ChangeTracker.ChangeTrackingEnabled)
            {
                if (ChangeTracker.OriginalValues.ContainsKey("TB_AC_VACINA_DOSE")
                    && (ChangeTracker.OriginalValues["TB_AC_VACINA_DOSE"] == TB_AC_VACINA_DOSE))
                {
                    ChangeTracker.OriginalValues.Remove("TB_AC_VACINA_DOSE");
                }
                else
                {
                    ChangeTracker.RecordOriginalValue("TB_AC_VACINA_DOSE", previousValue);
                }
                if (TB_AC_VACINA_DOSE != null && !TB_AC_VACINA_DOSE.ChangeTracker.ChangeTrackingEnabled)
                {
                    TB_AC_VACINA_DOSE.StartTracking();
                }
            }
        }

        private void FixupTB_AC_VACINA_DOSE1(TB_AC_VACINA_DOSE previousValue, bool skipKeys = false)
        {
            if (IsDeserializing)
            {
                return;
            }

            if (previousValue != null && previousValue.TB_AC_VACINA_DOSE_REGRA1.Contains(this))
            {
                previousValue.TB_AC_VACINA_DOSE_REGRA1.Remove(this);
            }

            if (TB_AC_VACINA_DOSE1 != null)
            {
                if (!TB_AC_VACINA_DOSE1.TB_AC_VACINA_DOSE_REGRA1.Contains(this))
                {
                    TB_AC_VACINA_DOSE1.TB_AC_VACINA_DOSE_REGRA1.Add(this);
                }

                CD_VACINA_REGRA = TB_AC_VACINA_DOSE1.CD_VACINA;
                CD_DOSE_VACINA_REGRA = TB_AC_VACINA_DOSE1.CD_DOSE_VACINA;
            }
            else if (!skipKeys)
            {
                CD_VACINA_REGRA = null;
                CD_DOSE_VACINA_REGRA = null;
            }

            if (ChangeTracker.ChangeTrackingEnabled)
            {
                if (ChangeTracker.OriginalValues.ContainsKey("TB_AC_VACINA_DOSE1")
                    && (ChangeTracker.OriginalValues["TB_AC_VACINA_DOSE1"] == TB_AC_VACINA_DOSE1))
                {
                    ChangeTracker.OriginalValues.Remove("TB_AC_VACINA_DOSE1");
                }
                else
                {
                    ChangeTracker.RecordOriginalValue("TB_AC_VACINA_DOSE1", previousValue);
                }
                if (TB_AC_VACINA_DOSE1 != null && !TB_AC_VACINA_DOSE1.ChangeTracker.ChangeTrackingEnabled)
                {
                    TB_AC_VACINA_DOSE1.StartTracking();
                }
            }
        }

        #endregion
    }
}


text:
require 'spec_helper'
require 'vk/api/places/methods/get_checkins'

RSpec.describe Vk::API::Places::Methods::GetCheckins do
  subject(:model) { described_class }
  it { is_expected.to be < Dry::Struct }
  it { is_expected.to be < Vk::Schema::Method }

  describe 'attributes' do
    subject(:attributes) { model.instance_methods(false) }

    it { is_expected.to include :latitude }
    it { is_expected.to include :longitude }
    it { is_expected.to include :place }
    it { is_expected.to include :user_id }
    it { is_expected.to include :offset }
    it { is_expected.to include :count }
    it { is_expected.to include :timestamp }
    it { is_expected.to include :friends_only }
    it { is_expected.to include :need_places }
  end
end


text:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>basic_outcome(ErrorCondEnum &&) - Boost.Outcome documentation</title>
<link rel="stylesheet" href="../../../css/boost.css" type="text/css">
<meta name="generator" content="Hugo 0.52 with Boostdoc theme">
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>

<link rel="icon" href="../../../images/favicon.ico" type="image/ico"/>

basic_outcome(ErrorCondEnum &&)

Implicit error_type from ErrorCondEnum constructor. Calls void on_outcome_construction(T *, U &&) noexcept
 with this and ErrorCondEnum.

Requires: predicate::enable_error_condition_converting_constructor<R> is true.

Complexity: Same as for error_type’s copy or move constructor from the result of make_error_code(ErrorCondEnum). Constexpr, triviality and noexcept of underlying operations is propagated.

Guarantees: If an exception is thrown during the operation, the state of the input is left indeterminate.

Last revised: December 17, 2020 at 11:27:06 UTC


text:
require 'spec_helper'

describe WP2Middleman::Post do
  let(:file) { Nokogiri::XML(File.open("#{ENV['PWD']}/spec/fixtures/fixture.xml")) }
  let(:post_one) { WP2Middleman::Post.new(file.css('item')[0]) }
  let(:post_two) { WP2Middleman::Post.new(file.css('item')[1]) }
  let(:post_three) { WP2Middleman::Post.new(file.css('item')[2]) }

  it "exists as a class within the WP2Middleman module" do
    expect(WP2Middleman::Post.class).to eq Class
  end

  describe "#title" do
    subject { post_one.title }

    it { is_expected.to eq "A Title" }
  end

  describe "#post_date" do
    subject { post_one.post_date }

    it { is_expected.to eq "2012-06-08 03:21:41" }
  end

  describe "#date_published" do
    subject { post_one.date_published }

    it { is_expected.to eq "2012-06-08" }
  end

  describe "#date_time_published" do
    subject { post_one.date_time_published }

    it { is_expected.to eq "2012-06-08 03:21:41" }
  end

  describe "#status" do
    subject { post_three.status }

    it { is_expected.to eq "private" }
  end

  describe "#field" do
    subject { post_one.field('wp:post_id') }

    it { is_expected.to eq "84" }
  end

  describe "#published?" do
    subject { post_one.published? }

    it { is_expected.to eq true }

    context "#status is not 'publish'" do
      subject { post_three.published? }

      it { is_expected.to eq false }

    end
  end

  describe "#content" do
    subject { post_one.content }

    it { is_expected.to eq "Paragraph one.\n\n      Paragraph two.\n    " }
  end

  describe "#tags" do
    subject { post_two.tags }

    it { is_expected.to eq ["some_tag", "another tag", "tag"] }

    context "the post only has an 'Uncategorized' tag" do
      subject { post_one.tags }

      it { is_expected.to eq [] }
    end
  end

  describe "#valid?" do
    def post(post_date: Date.new(2014,2,19), title: "Title", date_time_published: Time.new(2014,2,19,1,2,3), content: "content")
      post = WP2Middleman::Post.new(double)

      allow(post).to receive(:post_date) { post_date }
      allow(post).to receive(:title) { title }
      allow(post).to receive(:date_time_published) { date_time_published }
      allow(post).to receive(:content) { content }

      post
    end

    it "is valid with post_date, title, date_published, and content" do
      expect(post).to be_valid
    end

    it "is not valid without post_date" do
      expect(post(post_date: nil)).to_not be_valid
    end

    it "is not valid without a title" do
      expect(post(title: nil)).to_not be_valid
    end

    it "is not valid without a date_published" do
      expect(post(date_time_published: nil)).to_not be_valid
    end

    it "is not valid without content" do
      expect(post(content: nil)).to_not be_valid
    end
  end
end


text:
<?php

namespace Tmdb\Factory;

use Tmdb\Factory\Common\ChangeFactory;
use Tmdb\Factory\Common\VideoFactory;
use Tmdb\Factory\People\CastFactory;
use Tmdb\Factory\People\CrewFactory;
use Tmdb\HttpClient\HttpClient;
use Tmdb\Model\AbstractModel;
use Tmdb\Model\Common\Country;
use Tmdb\Model\Common\ExternalIds;
use Tmdb\Model\Common\GenericCollection;
use Tmdb\Model\Common\SpokenLanguage;
use Tmdb\Model\Common\Translation;
use Tmdb\Model\Company;
use Tmdb\Model\Network;
use Tmdb\Model\Person\CastMember;
use Tmdb\Model\Person\CrewMember;
use Tmdb\Model\Tv;
use Tmdb\Model\Watch;

/**
 * Class TvFactory
 * @package Tmdb\Factory
 */
class TvFactory extends AbstractFactory
{
    /**
     * @var People\CastFactory
     */
    private $castFactory;

    /**
     * @var People\CrewFactory
     */
    private $crewFactory;

    /**
     * @var GenreFactory
     */
    private $genreFactory;

    /**
     * @var ImageFactory
     */
    private $imageFactory;

    /**
     * @var TvSeasonFactory
     */
    private $tvSeasonFactory;

    /**
     * @var TvEpisodeFactory
     */
    private $tvEpisodeFactory;

    /**
     * @var NetworkFactory
     */
    private $networkFactory;

    /**
     * @var Common\VideoFactory
     */
    private $videoFactory;

    /**
     * @var ChangeFactory
     */
    private $changesFactory;

    /**
     * @var KeywordFactory
     */
    private $keywordFactory;

    /**
     * Constructor
     *
     * @param HttpClient $httpClient
     */
    public function __construct(HttpClient $httpClient)
    {
        $this->castFactory = new CastFactory($httpClient);
        $this->crewFactory = new CrewFactory($httpClient);
        $this->genreFactory = new GenreFactory($httpClient);
        $this->imageFactory = new ImageFactory($httpClient);
        $this->tvSeasonFactory = new TvSeasonFactory($httpClient);
        $this->tvEpisodeFactory = new TvEpisodeFactory($httpClient);
        $this->networkFactory = new NetworkFactory($httpClient);
        $this->videoFactory = new VideoFactory($httpClient);
        $this->changesFactory = new ChangeFactory($httpClient);
        $this->keywordFactory = new KeywordFactory($httpClient);

        parent::__construct($httpClient);
    }

    /**
     * {@inheritdoc}
     */
    public function createCollection(array $data = [])
    {
        $collection = new GenericCollection();

        if (array_key_exists('results', $data)) {
            $data = $data['results'];
        }

        foreach ($data as $item) {
            $collection->add(null, $this->create($item));
        }

        return $collection;
    }

    /**
     * @param array $data
     *
     * @return Tv|null
     */
    public function create(array $data = []): ?Tv
    {
        if (!$data) {
            return null;
        }

        $tvShow = new Tv();

        if (array_key_exists('content_ratings', $data) && array_key_exists('results', $data['content_ratings'])) {
            $tvShow->setContentRatings(
                $this->createGenericCollection($data['content_ratings']['results'], new Tv\ContentRating())
            );
        }

        if (array_key_exists('credits', $data)) {
            if (array_key_exists('cast', $data['credits']) && $data['credits']['cast'] !== null) {
                $tvShow->getCredits()->setCast(
                    $this->getCastFactory()->createCollection(
                        $data['credits']['cast'],
                        new CastMember()
                    )
                );
            }

            if (array_key_exists('crew', $data['credits']) && $data['credits']['crew'] !== null) {
                $tvShow->getCredits()->setCrew(
                    $this->getCrewFactory()->createCollection(
                        $data['credits']['crew'],
                        new CrewMember()
                    )
                );
            }
        }

        /** External ids */
        if (array_key_exists('external_ids', $data) && $data['external_ids'] !== null) {
            $tvShow->setExternalIds(
                $this->hydrate(new ExternalIds(), $data['external_ids'])
            );
        }

        /** Genres */
        if (array_key_exists('genres', $data) && $data['genres'] !== null) {
            $tvShow->setGenres($this->getGenreFactory()->createCollection($data['genres']));
        }

        /** Genres */
        if (array_key_exists('genre_ids', $data)) {
            $formattedData = [];

            foreach ($data['genre_ids'] as $genreId) {
                $formattedData[] = [
                    'id' => $genreId
                ];
            }

            $tvShow->setGenres($this->getGenreFactory()->createCollection($formattedData));
        }

        /** Images */
        if (array_key_exists('images', $data) && $data['images'] !== null) {
            $tvShow->setImages(
                $this->getImageFactory()->createCollectionFromTv($data['images'])
            );
        }

        if (array_key_exists('backdrop_path', $data)) {
            $tvShow->setBackdropImage(
                $this->getImageFactory()->createFromPath($data['backdrop_path'], 'backdrop_path')
            );
        }

        if (array_key_exists('poster_path', $data)) {
            $tvShow->setPosterImage(
                $this->getImageFactory()->createFromPath($data['poster_path'], 'poster_path')
            );
        }

        /** Translations */
        if (array_key_exists('translations', $data) && null !== $data['translations']) {
            if (array_key_exists('translations', $data['translations'])) {
                $translations = $data['translations']['translations'];
            } else {
                $translations = $data['translations'];
            }

            $tvShow->setTranslations(
                $this->createGenericCollection($translations, new Translation())
            );
        }

        /** Seasons */
        if (array_key_exists('seasons', $data) && $data['seasons'] !== null) {
            $tvShow->setSeasons($this->getTvSeasonFactory()->createCollection($data['seasons']));
        }

        /** Episodes **/
        if (array_key_exists('last_episode_to_air', $data) && $data['last_episode_to_air'] !== null) {
            $tvShow->setLastEpisodeToAir($this->getTvEpisodeFactory()->create($data['last_episode_to_air']));
        }
        if (array_key_exists('next_episode_to_air', $data) && $data['next_episode_to_air'] !== null) {
            $tvShow->setNextEpisodeToAir($this->getTvEpisodeFactory()->create($data['next_episode_to_air']));
        }

        /** Networks */
        if (array_key_exists('networks', $data) && $data['networks'] !== null) {
            $tvShow->setNetworks($this->getNetworkFactory()->createCollection($data['networks']));
        }

        if (array_key_exists('watch/providers', $data) && array_key_exists('results', $data['watch/providers'])) {
            $watchProviders = new GenericCollection();
            foreach ($data['watch/providers']['results'] as $iso31661 => $countryWatchData) {
                $countryWatchData['iso_3166_1'] = $iso31661;

                foreach (['flatrate', 'rent', 'buy'] as $providerType) {
                    $typeProviders = new GenericCollection();
                    foreach ($countryWatchData[$providerType] ?? [] as $providerData) {
                        if (isset($providerData['provider_id'])) {
                            $providerData['id'] = $providerData['provider_id'];
                        }
                        if (isset($providerData['provider_name'])) {
                            $providerData['name'] = $providerData['provider_name'];
                        }

                        $providerData['iso_3166_1'] = $iso31661;
                        $providerData['type'] = $providerType;
                        $typeProviders->add(null, $this->hydrate(new Watch\Provider(), $providerData));
                    }
                    $countryWatchData[$providerType] = $typeProviders;
                }

                $watchProviders->add($iso31661, $this->hydrate(new Watch\Providers(), $countryWatchData));
            }
            $tvShow->setWatchProviders($watchProviders);
        }

        if (array_key_exists('videos', $data) && $data['videos'] !== null) {
            $tvShow->setVideos($this->getVideoFactory()->createCollection($data['videos']));
        }

        if (array_key_exists('keywords', $data) && array_key_exists('results', $data['keywords'])) {
            $tvShow->setKeywords($this->getKeywordFactory()->createCollection($data['keywords']['results']));
        }

        if (array_key_exists('changes', $data) && $data['changes'] !== null) {
            $tvShow->setChanges($this->getChangesFactory()->createCollection($data['changes']));
        }

        if (array_key_exists('similar', $data) && $data['similar'] !== null) {
            $tvShow->setSimilar($this->createResultCollection($data['similar']));
        }

        if (array_key_exists('recommendations', $data) && $data['recommendations'] !== null) {
            $tvShow->setRecommendations($this->createResultCollection($data['recommendations']));
        }

        if (array_key_exists('languages', $data) && $data['languages'] !== null) {
            $collection = new GenericCollection();

            foreach ($data['languages'] as $iso6391) {
                $object = new SpokenLanguage();
                $object->setIso6391($iso6391);

                $collection->add(null, $object);
            }

            $tvShow->setLanguages($collection);
        }

        if (array_key_exists('origin_country', $data) && $data['origin_country'] !== null) {
            $collection = new GenericCollection();

            foreach ($data['origin_country'] as $iso31661) {
                $object = new Country();
                $object->setIso31661($iso31661);

                $collection->add(null, $object);
            }

            $tvShow->setOriginCountry($collection);
        }

        if (array_key_exists('production_companies', $data)) {
            $tvShow->setProductionCompanies(
                $this->createGenericCollection($data['production_companies'], new Company())
            );
        }

        if (array_key_exists('created_by', $data) && $data['created_by'] !== null) {
            $collection = new GenericCollection();
            $factory = new PeopleFactory($this->getHttpClient());

            foreach ($data['created_by'] as $castMember) {
                $object = $factory->create($castMember, new CastMember());

                $collection->add(null, $object);
            }

            $tvShow->setCreatedBy($collection);
        }

        if (array_key_exists('alternative_titles', $data) && array_key_exists('results', $data['alternative_titles'])) {
            $tvShow->setAlternativeTitles(
                $this->createGenericCollection($data['alternative_titles']['results'], new Tv\AlternativeTitle())
            );
        }

        if (array_key_exists('episode_groups', $data) && array_key_exists('results', $data['episode_groups'])) {
            $episodeGroupCollection = new GenericCollection();

            foreach ($data['episode_groups']['results'] as $episodeGroup) {
                if (!is_null($episodeGroup['network'])) {
                    $episodeGroup['network'] = $this->hydrate(new Network(), $episodeGroup['network']);
                }

                $episodeGroupCollection->add(null, $this->hydrate(new Tv\EpisodeGroups(), $episodeGroup));
            }
            $tvShow->setEpisodeGroups($episodeGroupCollection);
        }

        return $this->hydrate($tvShow, $data);
    }

    /**
     * @return CastFactory
     */
    public function getCastFactory()
    {
        return $this->castFactory;
    }

    /**
     * @param CastFactory $castFactory
     * @return self
     */
    public function setCastFactory($castFactory)
    {
        $this->castFactory = $castFactory;

        return $this;
    }

    /**
     * @return CrewFactory
     */
    public function getCrewFactory()
    {
        return $this->crewFactory;
    }

    /**
     * @param CrewFactory $crewFactory
     * @return self
     */
    public function setCrewFactory($crewFactory)
    {
        $this->crewFactory = $crewFactory;

        return $this;
    }

    /**
     * @return GenreFactory
     */
    public function getGenreFactory()
    {
        return $this->genreFactory;
    }

    /**
     * @param GenreFactory $genreFactory
     * @return self
     */
    public function setGenreFactory($genreFactory)
    {
        $this->genreFactory = $genreFactory;

        return $this;
    }

    /**
     * @return ImageFactory
     */
    public function getImageFactory()
    {
        return $this->imageFactory;
    }

    /**
     * @param ImageFactory $imageFactory
     * @return self
     */
    public function setImageFactory($imageFactory)
    {
        $this->imageFactory = $imageFactory;

        return $this;
    }

    /**
     * @return TvSeasonFactory
     */
    public function getTvSeasonFactory()
    {
        return $this->tvSeasonFactory;
    }

    /**
     * @param TvSeasonFactory $tvSeasonFactory
     * @return self
     */
    public function setTvSeasonFactory($tvSeasonFactory)
    {
        $this->tvSeasonFactory = $tvSeasonFactory;

        return $this;
    }

    /**
     * @return TvEpisodeFactory
     */
    public function getTvEpisodeFactory()
    {
        return $this->tvEpisodeFactory;
    }

    /**
     * @param TvEpisodeFactory $tvEpisodeFactory
     * @return self
     */
    public function setTvEpisodeFactory($tvEpisodeFactory)
    {
        $this->tvEpisodeFactory = $tvEpisodeFactory;

        return $this;
    }

    /**
     * @return NetworkFactory
     */
    public function getNetworkFactory()
    {
        return $this->networkFactory;
    }

    /**
     * @param NetworkFactory $networkFactory
     * @return self
     */
    public function setNetworkFactory($networkFactory)
    {
        $this->networkFactory = $networkFactory;

        return $this;
    }

    /**
     * @return VideoFactory
     */
    public function getVideoFactory()
    {
        return $this->videoFactory;
    }

    /**
     * @param VideoFactory $videoFactory
     * @return self
     */
    public function setVideoFactory($videoFactory)
    {
        $this->videoFactory = $videoFactory;

        return $this;
    }

    /**
     * @return KeywordFactory
     */
    public function getKeywordFactory()
    {
        return $this->keywordFactory;
    }

    /**
     * @param KeywordFactory $keywordFactory
     * @return self
     */
    public function setKeywordFactory($keywordFactory)
    {
        $this->keywordFactory = $keywordFactory;

        return $this;
    }

    /**
     * @return ChangeFactory
     */
    public function getChangesFactory()
    {
        return $this->changesFactory;
    }

    /**
     * @param ChangeFactory $changesFactory
     * @return self
     */
    public function setChangesFactory($changesFactory)
    {
        $this->changesFactory = $changesFactory;

        return $this;
    }
}


text:
@interface XMGHTTPSessionManager : AFHTTPSessionManager

@end


text:
@implementation UserModel {

}

+ (NSDictionary *)modelCustomPropertyMapper {
    return @{@"uid" : @"uid",
            @"username" : @"username",
            @"nickname" : @"nickname",
            @"createdAt" : @"created_at"};
}

@end


text:
package com.epimorphics.server.webapi;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;

import org.apache.commons.collections.map.LRUMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.epimorphics.server.core.ServiceConfig;
import com.epimorphics.server.core.Store;
import com.epimorphics.server.webapi.impl.ArrayDatatable;
import com.epimorphics.server.webapi.impl.DatatableProjection;
import com.epimorphics.server.webapi.impl.DatatableResponse;
import com.epimorphics.server.webapi.impl.TableResourcePair;
import com.epimorphics.util.EpiException;
import com.epimorphics.util.PrefixUtils;
import com.hp.hpl.jena.query.QueryExecution;
import com.hp.hpl.jena.query.QueryExecutionFactory;
import com.hp.hpl.jena.query.QuerySolution;
import com.hp.hpl.jena.query.ResultSetFactory;
import com.hp.hpl.jena.query.ResultSetRewindable;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.RDFNode;

@Path("system/tables")
public class DatatableAPI extends BaseEndpoint {
    static Logger log = LoggerFactory.getLogger(DatatableAPI.class);

    protected static final String PARAM_query   = "query";
    protected static final String PARAM_columns = "cols";
    protected static final String PARAM_store   = "store";

    static LRUMap tableCache = new LRUMap(50);

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public DatatableResponse projectTable() {
        MultivaluedMap<String, String> parameters = uriInfo.getQueryParameters();

        String query = getSafeParam(PARAM_query);
        String columns = getSafeParam(PARAM_columns);
        String storename = getSafeParam(PARAM_store);

        String key = query + "|" + columns;

        ArrayDatatable table = null;
        synchronized (tableCache) {
            table = (ArrayDatatable) tableCache.get(key);
        }
        if (table == null) {
            table = initializeTable(storename, query, columns);
            synchronized (tableCache) {
                tableCache.put(key, table);
            }
        }

        DatatableProjection projection = new DatatableProjection( flatten(parameters) );

        return table.project(projection);
    }

    private String getSafeParam(String name) {
        String val = uriInfo.getQueryParameters().getFirst(name);
        if (val == null || val.isEmpty()) {
            throw new EpiException("Missing required parameter: " + name);
        }
        return val;
    }

    private ArrayDatatable initializeTable(String storename, String query, String columns) {
        log.debug("Creating datatable cache for: " + query + " | " + columns);
        String[] varnamesRaw = columns.split(",");
        Object[] varnames = new Object[varnamesRaw.length];
        for (int i = 0; i < varnames.length; i++) {
            String var = varnamesRaw[i].trim();
            if (var.contains(">")) {
                String[] varPair = var.split(">");
                varnames[i] = new TableResourcePair(varPair[0].trim(), varPair[1].trim());
            } else {
                varnames[i] = var;
            }
        }

        Store store = ServiceConfig.get().getServiceAs(storename, Store.class);
        if (store == null) {
            throw new EpiException("Can't find requested store: " + storename);
        }

        Model m = store.getUnionModel();
        String q = PrefixUtils.expandQuery(query, m);
        QueryExecution qexec = QueryExecutionFactory.create(q, m);
        store.lock();
        ResultSetRewindable results = null;
        try {
            results = ResultSetFactory.copyResults(qexec.execSelect());
        } finally {
            qexec.close();
            store.unlock();
        }
        Object[][] data = new Object[results.size()][];
        int count = 0;
        while (results.hasNext()) {
            QuerySolution soln = results.next();
            Object[] row = new Object[varnames.length];
            for (int i = 0; i < varnames.length; i++) {
                try {
                    Object varname = varnames[i];
                    if (varname instanceof TableResourcePair) {
                        TableResourcePair pair = (TableResourcePair)varname;
                        String name = soln.get(pair.getName()).toString();
                        String uri = soln.get(pair.getUri()).toString();
                        row[i] = new TableResourcePair(name, uri);
                    } else {
                        row[i] = nodeValue(soln.get((String)varname));
                    }
                } catch (Exception e) {
                    row[i] = "internal error";
                }
            }
            data[count++] = row;
        }

        return new ArrayDatatable(data);
    }

    private Object nodeValue(RDFNode val) {
        if (val.isLiteral()) {
            return val.asLiteral().getValue();
        } else if (val.isURIResource()) {
            return val.asResource().getURI();
        } else {
            return "[]";
        }
    }

    private Map<String, String> flatten(MultivaluedMap<String, String> parameters) {
        Map<String, String> flatParams = new HashMap<String, String>();
        for (String key : parameters.keySet()) {
            flatParams.put(key, parameters.getFirst(key));
        }
        return flatParams;
    }
}