<commit_before>// @flow
import * as Constants from '../constants/settings'
import {apiserverGetRpcPromise, apiserverPostRpcPromise, apiserverPostJSONRpcPromise, loginAccountDeleteRpcPromise} from '../constants/types/flow-types'
import {setDeletedSelf} from '../actions/login'
import {call, put, select, fork, cancel} from 'redux-saga/effects'
import {takeEvery, takeLatest, delay} from 'redux-saga'
import {routeAppend} from '../actions/router'

import type {SagaGenerator} from '../constants/types/saga'
import type {
  DeleteAccountForever,
  InvitesReclaim,
  InvitesReclaimed,
  InvitesRefresh,
  InvitesSend,
  InvitesSent,
  NotificationsRefresh,
  NotificationsSave,
  NotificationsToggle,
  SetAllowDeleteAccount,
} from '../constants/settings'

function invitesReclaim (inviteId: string): InvitesReclaim {
  return {type: Constants.invitesReclaim, payload: {inviteId}}
}

function invitesRefresh (): InvitesRefresh {
  return {type: Constants.invitesRefresh, payload: undefined}
}

function invitesSend (email: string, message: ?string): InvitesSend {
  return {type: Constants.invitesSend, payload: {email, message}}
}

function notificationsRefresh (): NotificationsRefresh {
  return {type: Constants.notificationsRefresh, payload: undefined}
}

function notificationsSave (): NotificationsSave {
  return {type: Constants.notificationsSave, payload: undefined}
}

function notificationsToggle (name?: string): NotificationsToggle {
  return {type: Constants.notificationsToggle, payload: {name}}
}

function setAllowDeleteAccount (allow: boolean): SetAllowDeleteAccount {
  return {type: Constants.setAllowDeleteAccount, payload: allow}
}

function deleteAccountForever (): DeleteAccountForever {
  return {type: Constants.deleteAccountForever, payload: undefined}
}

function * saveNotificationsSaga (): SagaGenerator<any, any> {
  try {
    const current = yield select(state => state.settings.notifications)

    if (!current || !current.settings) {
      throw new Error('No notifications loaded yet')
    }

    const JSONPayload = current.settings
      .map(s => ({
        key: `${s.name}|email`,
        value: s.subscribed ? '1' : '0'}))
      .concat({
        key: `unsub|email`,
        value: current.unsubscribedFromAll ? '1' : '0'})

    const result = yield call(apiserverPostJSONRpcPromise, {
      param: {
        endpoint: 'account/subscribe',
        args: [],
        JSONPayload,
      },
    })

    if (!result || !result.body || JSON.parse(result.body).status.code !== 0) {
      throw new Error(`Invalid response ${result || '(no result)'}`)
    }

    yield put({
      type: Constants.notificationsSaved,
      payload: undefined,
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * reclaimInviteSaga (invitesReclaimAction: InvitesReclaim): SagaGenerator<any, any> {
  const {inviteId} = invitesReclaimAction.payload
  try {
    yield call(apiserverPostRpcPromise, {
      param: {
        endpoint: 'cancel_invitation',
        args: [{key: 'invitation_id', value: inviteId}],
      },
    })
    yield put(({
      type: Constants.invitesReclaimed,
      payload: undefined,
    }: InvitesReclaimed))
  } catch (e) {
    console.warn('Error reclaiming an invite:', e)
    yield put(({
      type: Constants.invitesReclaimed,
      payload: {errorText: e.desc + e.name, errorObj: e},
      error: true,
    }: InvitesReclaimed))
  }
  yield put(invitesRefresh())
}

function * refreshInvitesSaga (): SagaGenerator<any, any> {
  try {
    const json: ?{body: string} = yield call(apiserverGetRpcPromise, {
      param: {
        endpoint: 'invitations_sent',
        args: [],
      },
    })

    const results: {
      invitations: [{
        assertion: ?string,
        ctime: number,
        email: string,
        invitation_id: string,
        short_code: string,
        type: string,
        uid: string,
        username: string,
      }],
    } = JSON.parse(json && json.body || '')

    const acceptedInvites = []
    const pendingInvites = []

    results.invitations.forEach(i => {
      const invite: Constants.Invitation = {
        created: i.ctime,
        email: i.email,
        id: i.invitation_id,
        key: i.invitation_id,
        // type will get filled in later
        type: '',
        username: i.username,
        uid: i.uid,
        // First ten chars of invite code is sufficient
        url: 'keybase.io/inv/' + i.invitation_id.slice(0, 10),
      }
      // Here's an algorithm for interpreting invitation entries.
      // 1: username+uid => accepted invite, else
      // 2: email set => pending email invite, else
      // 3: pending invitation code invite
      if (i.username && i.uid) {
        invite.type = 'accepted'
        acceptedInvites.push(invite)
      } else {
        invite.type = i.email ? 'pending-email' : 'pending-url'
        pendingInvites.push(invite)
      }
    })
    yield put({
      type: Constants.invitesRefreshed,
      payload: {
        acceptedInvites,
        pendingInvites,
      },
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * sendInviteSaga (invitesSendAction: InvitesSend): SagaGenerator<any, any> {
  const {email, message} = invitesSendAction.payload
  const args = [{key: 'email', value: email}]
  if (message) {
    args.push({key: 'invitation_message', value: message})
  }
  try {
    const response: ?{body: string} = yield call(apiserverPostRpcPromise, {
      param: {
        endpoint: 'send_invitation',
        args,
      },
    })
    if (response) {
      const parsedBody = JSON.parse(response.body)
      yield put(({
        type: Constants.invitesSent,
        payload: {email, invitationId: parsedBody.invitation_id},
      }: InvitesSent))
      yield put(routeAppend('invitesent'))
    }
  } catch (e) {
    console.warn('Error sending an invite:', e)
    yield put(({
      type: Constants.invitesSent,
      payload: {errorText: e.desc + e.name, errorObj: e},
      error: true,
    }: InvitesSent))
  }
  yield put(invitesRefresh())
}

function * refreshNotificationsSaga (): SagaGenerator<any, any> {
  try {
    // If the rpc is fast don't clear it out first
    const delayThenEmptyTask = yield fork(function * () {
      yield call(delay, 500)
      yield put({
        type: Constants.notificationsRefreshed,
        payload: {
          settings: null,
          unsubscribedFromAll: null,
        }})
    })

    const json: ?{body: string} = yield call(apiserverGetRpcPromise, {
      param: {
        endpoint: 'account/subscriptions',
        args: [],
      },
    })

    yield cancel(delayThenEmptyTask)

    const results: {
      notifications: {
        email: {
          settings: Array<{
            name: string,
            description: string,
            subscribed: boolean,
          }>,
          unsub: boolean,
        },
      },
    } = JSON.parse(json && json.body || '')

    const unsubscribedFromAll = results.notifications.email.unsub

    const settings = results.notifications.email.settings.map(s => ({
      name: s.name,
      subscribed: s.subscribed,
      description: s.description,
    })) || []

    yield put({
      type: Constants.notificationsRefreshed,
      payload: {
        unsubscribedFromAll,
        settings,
      },
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * deleteAccountForeverSaga (): SagaGenerator<any, any> {
  try {
    const username = yield select(state => state.config.username)
    const allowDeleteAccount = yield select(state => state.settings.allowDeleteAccount)

    if (!username) {
      throw new Error('Unable to delete account: not username set')
    }

    if (!allowDeleteAccount) {
      throw new Error('Account deletion failsafe was not disengaged. This is a bug!')
    }

    yield call(loginAccountDeleteRpcPromise)
    yield put(setDeletedSelf(username))
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * settingsSaga (): SagaGenerator<any, any> {
  yield [
    takeEvery(Constants.invitesReclaim, reclaimInviteSaga),
    takeLatest(Constants.invitesRefresh, refreshInvitesSaga),
    takeEvery(Constants.invitesSend, sendInviteSaga),
    takeLatest(Constants.notificationsRefresh, refreshNotificationsSaga),
    takeLatest(Constants.notificationsSave, saveNotificationsSaga),
    takeLatest(Constants.deleteAccountForever, deleteAccountForeverSaga),
  ]
}

export {
  invitesReclaim,
  invitesRefresh,
  invitesSend,
  notificationsRefresh,
  notificationsSave,
  notificationsToggle,
  setAllowDeleteAccount,
  deleteAccountForever,
}

export default settingsSaga
<commit_msg>only ten chars of invitation_id<commit_after>// @flow
import * as Constants from '../constants/settings'
import {apiserverGetRpcPromise, apiserverPostRpcPromise, apiserverPostJSONRpcPromise, loginAccountDeleteRpcPromise} from '../constants/types/flow-types'
import {setDeletedSelf} from '../actions/login'
import {call, put, select, fork, cancel} from 'redux-saga/effects'
import {takeEvery, takeLatest, delay} from 'redux-saga'
import {routeAppend} from '../actions/router'

import type {SagaGenerator} from '../constants/types/saga'
import type {
  DeleteAccountForever,
  InvitesReclaim,
  InvitesReclaimed,
  InvitesRefresh,
  InvitesSend,
  InvitesSent,
  NotificationsRefresh,
  NotificationsSave,
  NotificationsToggle,
  SetAllowDeleteAccount,
} from '../constants/settings'

function invitesReclaim (inviteId: string): InvitesReclaim {
  return {type: Constants.invitesReclaim, payload: {inviteId}}
}

function invitesRefresh (): InvitesRefresh {
  return {type: Constants.invitesRefresh, payload: undefined}
}

function invitesSend (email: string, message: ?string): InvitesSend {
  return {type: Constants.invitesSend, payload: {email, message}}
}

function notificationsRefresh (): NotificationsRefresh {
  return {type: Constants.notificationsRefresh, payload: undefined}
}

function notificationsSave (): NotificationsSave {
  return {type: Constants.notificationsSave, payload: undefined}
}

function notificationsToggle (name?: string): NotificationsToggle {
  return {type: Constants.notificationsToggle, payload: {name}}
}

function setAllowDeleteAccount (allow: boolean): SetAllowDeleteAccount {
  return {type: Constants.setAllowDeleteAccount, payload: allow}
}

function deleteAccountForever (): DeleteAccountForever {
  return {type: Constants.deleteAccountForever, payload: undefined}
}

function * saveNotificationsSaga (): SagaGenerator<any, any> {
  try {
    const current = yield select(state => state.settings.notifications)

    if (!current || !current.settings) {
      throw new Error('No notifications loaded yet')
    }

    const JSONPayload = current.settings
      .map(s => ({
        key: `${s.name}|email`,
        value: s.subscribed ? '1' : '0'}))
      .concat({
        key: `unsub|email`,
        value: current.unsubscribedFromAll ? '1' : '0'})

    const result = yield call(apiserverPostJSONRpcPromise, {
      param: {
        endpoint: 'account/subscribe',
        args: [],
        JSONPayload,
      },
    })

    if (!result || !result.body || JSON.parse(result.body).status.code !== 0) {
      throw new Error(`Invalid response ${result || '(no result)'}`)
    }

    yield put({
      type: Constants.notificationsSaved,
      payload: undefined,
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * reclaimInviteSaga (invitesReclaimAction: InvitesReclaim): SagaGenerator<any, any> {
  const {inviteId} = invitesReclaimAction.payload
  try {
    yield call(apiserverPostRpcPromise, {
      param: {
        endpoint: 'cancel_invitation',
        args: [{key: 'invitation_id', value: inviteId}],
      },
    })
    yield put(({
      type: Constants.invitesReclaimed,
      payload: undefined,
    }: InvitesReclaimed))
  } catch (e) {
    console.warn('Error reclaiming an invite:', e)
    yield put(({
      type: Constants.invitesReclaimed,
      payload: {errorText: e.desc + e.name, errorObj: e},
      error: true,
    }: InvitesReclaimed))
  }
  yield put(invitesRefresh())
}

function * refreshInvitesSaga (): SagaGenerator<any, any> {
  try {
    const json: ?{body: string} = yield call(apiserverGetRpcPromise, {
      param: {
        endpoint: 'invitations_sent',
        args: [],
      },
    })

    const results: {
      invitations: [{
        assertion: ?string,
        ctime: number,
        email: string,
        invitation_id: string,
        short_code: string,
        type: string,
        uid: string,
        username: string,
      }],
    } = JSON.parse(json && json.body || '')

    const acceptedInvites = []
    const pendingInvites = []

    results.invitations.forEach(i => {
      const invite: Constants.Invitation = {
        created: i.ctime,
        email: i.email,
        id: i.invitation_id,
        key: i.invitation_id,
        // type will get filled in later
        type: '',
        username: i.username,
        uid: i.uid,
        // First ten chars of invite code is sufficient
        url: 'keybase.io/inv/' + i.invitation_id.slice(0, 10),
      }
      // Here's an algorithm for interpreting invitation entries.
      // 1: username+uid => accepted invite, else
      // 2: email set => pending email invite, else
      // 3: pending invitation code invite
      if (i.username && i.uid) {
        invite.type = 'accepted'
        acceptedInvites.push(invite)
      } else {
        invite.type = i.email ? 'pending-email' : 'pending-url'
        pendingInvites.push(invite)
      }
    })
    yield put({
      type: Constants.invitesRefreshed,
      payload: {
        acceptedInvites,
        pendingInvites,
      },
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * sendInviteSaga (invitesSendAction: InvitesSend): SagaGenerator<any, any> {
  const {email, message} = invitesSendAction.payload
  const args = [{key: 'email', value: email}]
  if (message) {
    args.push({key: 'invitation_message', value: message})
  }
  try {
    const response: ?{body: string} = yield call(apiserverPostRpcPromise, {
      param: {
        endpoint: 'send_invitation',
        args,
      },
    })
    if (response) {
      const parsedBody = JSON.parse(response.body)
      yield put(({
        type: Constants.invitesSent,
        payload: {
          email,
          invitationId: parsedBody.invitation_id.slice(0, 10),
        },
      }: InvitesSent))
      yield put(routeAppend('invitesent'))
    }
  } catch (e) {
    console.warn('Error sending an invite:', e)
    yield put(({
      type: Constants.invitesSent,
      payload: {errorText: e.desc + e.name, errorObj: e},
      error: true,
    }: InvitesSent))
  }
  yield put(invitesRefresh())
}

function * refreshNotificationsSaga (): SagaGenerator<any, any> {
  try {
    // If the rpc is fast don't clear it out first
    const delayThenEmptyTask = yield fork(function * () {
      yield call(delay, 500)
      yield put({
        type: Constants.notificationsRefreshed,
        payload: {
          settings: null,
          unsubscribedFromAll: null,
        }})
    })

    const json: ?{body: string} = yield call(apiserverGetRpcPromise, {
      param: {
        endpoint: 'account/subscriptions',
        args: [],
      },
    })

    yield cancel(delayThenEmptyTask)

    const results: {
      notifications: {
        email: {
          settings: Array<{
            name: string,
            description: string,
            subscribed: boolean,
          }>,
          unsub: boolean,
        },
      },
    } = JSON.parse(json && json.body || '')

    const unsubscribedFromAll = results.notifications.email.unsub

    const settings = results.notifications.email.settings.map(s => ({
      name: s.name,
      subscribed: s.subscribed,
      description: s.description,
    })) || []

    yield put({
      type: Constants.notificationsRefreshed,
      payload: {
        unsubscribedFromAll,
        settings,
      },
    })
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * deleteAccountForeverSaga (): SagaGenerator<any, any> {
  try {
    const username = yield select(state => state.config.username)
    const allowDeleteAccount = yield select(state => state.settings.allowDeleteAccount)

    if (!username) {
      throw new Error('Unable to delete account: not username set')
    }

    if (!allowDeleteAccount) {
      throw new Error('Account deletion failsafe was not disengaged. This is a bug!')
    }

    yield call(loginAccountDeleteRpcPromise)
    yield put(setDeletedSelf(username))
  } catch (err) {
    // TODO hook into global error handler
    console.error(err)
  }
}

function * settingsSaga (): SagaGenerator<any, any> {
  yield [
    takeEvery(Constants.invitesReclaim, reclaimInviteSaga),
    takeLatest(Constants.invitesRefresh, refreshInvitesSaga),
    takeEvery(Constants.invitesSend, sendInviteSaga),
    takeLatest(Constants.notificationsRefresh, refreshNotificationsSaga),
    takeLatest(Constants.notificationsSave, saveNotificationsSaga),
    takeLatest(Constants.deleteAccountForever, deleteAccountForeverSaga),
  ]
}

export {
  invitesReclaim,
  invitesRefresh,
  invitesSend,
  notificationsRefresh,
  notificationsSave,
  notificationsToggle,
  setAllowDeleteAccount,
  deleteAccountForever,
}

export default settingsSaga
<|endoftext|>


<commit_before>/**
	_moon.Scroller_ extends [enyo.Scroller](#enyo.Scroller), adding support for
	5-way focus (Spotlight) and pagination buttons.

	_moon.Scroller_ responds to explicit/programmatic requests from
	controls to be scrolled into view via the _onRequestScrollIntoView_ event.
	These requests are generally made in response to the _onSpotlightFocused_
	event handled by these controls and ensures that 5-way (Spotlight) focused
	controls are in view.

	For more information, see the documentation on
	[Scrollers](building-apps/layout/scrollers.html) in the Enyo Developer Guide.
*/
enyo.kind({
	name:      "moon.Scroller",
	kind:      "enyo.Scroller",
	//* @public
	published: {
		//* If true, paging controls are hidden if a key is pressed (5-way mode)
		hidePagingOnKey: true,
		//* If true, paging controls are hidden if the user's pointer leaves this
		//* control
		hidePagingOnLeave: true,
		/**
			If true, when scrolling to focused child controls, the scroller will
			scroll as far as possible, until its edge meets the next item's edge
		*/
		scrollFullPage: false,
		/**
			If true, paging controls are focusable (in 5-way mode).  Normally, this
			is not required, since the scroller will automatically scroll to ensure
			most focusable items are in view.  It is intended to be used when the
			scroller contents have no spotlightable controls, such as the case of a
			scroller with a long body of text.
		*/
		spotlightPagingControls: false,
		//* Relative parameter used to determine scroll speed
		scrollInterval: 75,
		/**
			The ratio of mousewheel "delta" units to pixels scrolled. Increase this
			value to increase the distance scrolled by the scroll wheel. Note that
			mice/trackpads do not emit the same "delta" units per "notch" or flick of
			the scroll wheel/trackpad; that can vary based on intensity and momentum.
		*/
		scrollWheelMultiplier: 2,
		/**
			The ratio of the maximum distance scrolled by each scroll wheel event to
			the height/width of the viewport. Setting a value larger than 1 is not
			advised, since a single scroll event could move more than one viewport's
			worth of content (depending on the delta received), resulting in skipped
			content.
		*/
		scrollWheelPageMultiplier: 0.2,
		/**
			The ratio of the distance scrolled per tap of the paging button to the
			height/width of the viewport. Setting a value larger than 1 is not
			advised, since a paging button tap will move more than one viewport's
			worth of content, resulting in skipped content.
		*/
		paginationPageMultiplier: 0.8,
		/**
			The ratio of continuous-scrolling delta units to pixels scrolled. Increase
			this value to increase the distance scrolled when the pagination buttons
			are held.
		*/
		paginationScrollMultiplier: 8,
		/**
			When true, the scroll wheel moves spotlight focus up/down through the
			scroller when in 5-way mode. (In pointer mode, the scroll wheel always
			scrolls the viewport without modifying focus position.) When false, the
			scroll wheel works the same in 5-way mode as in pointer mode, where the
			wheel moves the position of the scroller viewport.
		*/
		scrollWheelMovesFocus: true
	},
	//* @protected
	handlers: {
		onSpotlightScrollUp:"spotlightWheel",
		onSpotlightScrollDown:"spotlightWheel",
		onSpotlightContainerEnter: "spotlightHello",
		onSpotlightFocus: "spotlightHello",
		onSpotlightContainerLeave: "spotlightGoodbye"
	},
	//* If true, scroll events are not allowed to propagate
	preventScrollPropagation: false,
	//* Default to moon.ScrollStrategy
	strategyKind: "moon.ScrollStrategy",
	/**
		Scrolls until _inControl_ is in view. If _inScrollFullPage_ is set, scrolls
		until the edge of _inControl_ is aligned with the edge of the visible scroll
		area. Optional third parameter to indicate whether or not it should animate
		the scroll. Defaults to animation unless it is set to false.
		If _setLastFocusedChild_ is true, scroller will set up _inControl_ to be the spotted child
		when scroller is spotted.
	*/
	scrollToControl: function(inControl, inScrollFullPage, animate, setLastFocusedChild) {
		if (setLastFocusedChild) {
			this.$.strategy.setLastFocusedChild(inControl);
		}
		this.$.strategy.animateToControl(inControl, inScrollFullPage, animate);
	},

	/**
		Accepts third optional paramater to indicate whether or not it should
		animate the scroll. Defaults to animation unless it is set to false.
	*/
	scrollTo: function (x, y, animate) {
		this.$.strategy.scrollTo(x, y, animate);
	},

	//* @protected
	bindings: [
		{from: ".scrollInterval",				to:".$.strategy.interval"},
		{from: ".scrollWheelMultiplier",		to:".$.strategy.scrollWheelMultiplier"},
		{from: ".scrollWheelPageMultiplier",	to:".$.strategy.scrollWheelPageMultiplier"},
		{from: ".paginationPageMultiplier",		to:".$.strategy.paginationPageMultiplier"},
		{from: ".paginationScrollMultiplier",	to:".$.strategy.paginationScrollMultiplier"}
	],
	create: function() {
		this.inherited(arguments);
		this.spotlightPagingControlsChanged();
		this.scrollWheelMovesFocusChanged();
	},
	spotlightPagingControlsChanged: function() {
		this.$.strategy.set("spotlightPagingControls", this.spotlightPagingControls);
	},
	scrollWheelMovesFocusChanged: function() {
		if (!this.scrollWheelMovesFocus) {
			this.setUseMouseWheel(true);
		}
	},
	spotlightWheel: function(inSender, inEvent) {
		if (this.scrollWheelMovesFocus) {
			if (!enyo.Spotlight.getPointerMode()) {
				var curr = enyo.Spotlight.getCurrent();
				if (curr && curr.isDescendantOf(this)) {
					var dir = inEvent.type == "onSpotlightScrollUp" ? "onSpotlightUp" : "onSpotlightDown";
					this._spotlightModal = this.spotlightModal;
					this.spotlightModal = true;	// Trap focus inside scroller while wheeling
					enyo.Spotlight.Util.dispatchEvent(dir, {type: dir}, curr);
					this.spotlightModal = this._spotlightModal;
					return true;
				}
			}
		}
	},
	// When scroller is entered or one of its children is focused
	// in 5-way mode, make sure that we're showing the scroll columns
	spotlightHello: function(inSender, inEvent) {
		if (this.$.strategy.showHideScrollColumns) {
			this.$.strategy.showHideScrollColumns(true);
		}
	},
	// When 5-way focus leaves scroller, hide the scroll columns
	spotlightGoodbye: function(inSender, inEvent) {
		if (inEvent.originator === this && this.$.strategy.showHideScrollColumns) {
			this.$.strategy.showHideScrollColumns(false);
		}
	},
	previewDomEvent: function(inEvent) {
		if (this.scrollWheelMovesFocus) {
			if (inEvent.type == "mousewheel") {
				this.setUseMouseWheel(enyo.Spotlight.getPointerMode());
			}
		}
	}
});

// On touch platforms, revert to using Enyo scroller, which picks an appropriate
// scroll strategy for the given platform
if (enyo.platform.touch) {
	moon.Scroller = enyo.Scroller;
}
<commit_msg>Editing for increased linearity<commit_after>/**
	_moon.Scroller_ extends [enyo.Scroller](#enyo.Scroller), adding support for
	5-way focus (Spotlight) and pagination buttons.

	_moon.Scroller_ responds when controls explicitly request to be scrolled into
	view by emitting the _onRequestScrollIntoView_ event. This typically happens
	when a control handles an _onSpotlightFocused_ event, ensuring that 5-way
	(Spotlight) focused controls remain in view.

	For more information, see the documentation on
	[Scrollers](building-apps/layout/scrollers.html) in the Enyo Developer Guide.
*/
enyo.kind({
	name:      "moon.Scroller",
	kind:      "enyo.Scroller",
	//* @public
	published: {
		//* If true, paging controls are hidden if a key is pressed (5-way mode)
		hidePagingOnKey: true,
		//* If true, paging controls are hidden if the user's pointer leaves this
		//* control
		hidePagingOnLeave: true,
		/**
			If true, when scrolling to focused child controls, the scroller will
			scroll as far as possible, until its edge meets the next item's edge
		*/
		scrollFullPage: false,
		/**
			If true, paging controls are focusable (in 5-way mode).  Normally, this
			is not required, since the scroller will automatically scroll to ensure
			most focusable items are in view.  It is intended to be used when the
			scroller contents have no spotlightable controls, such as the case of a
			scroller with a long body of text.
		*/
		spotlightPagingControls: false,
		//* Relative parameter used to determine scroll speed
		scrollInterval: 75,
		/**
			The ratio of mousewheel "delta" units to pixels scrolled. Increase this
			value to increase the distance scrolled by the scroll wheel. Note that
			mice/trackpads do not emit the same "delta" units per "notch" or flick of
			the scroll wheel/trackpad; that can vary based on intensity and momentum.
		*/
		scrollWheelMultiplier: 2,
		/**
			The ratio of the maximum distance scrolled by each scroll wheel event to
			the height/width of the viewport. Setting a value larger than 1 is not
			advised, since a single scroll event could move more than one viewport's
			worth of content (depending on the delta received), resulting in skipped
			content.
		*/
		scrollWheelPageMultiplier: 0.2,
		/**
			The ratio of the distance scrolled per tap of the paging button to the
			height/width of the viewport. Setting a value larger than 1 is not
			advised, since a paging button tap will move more than one viewport's
			worth of content, resulting in skipped content.
		*/
		paginationPageMultiplier: 0.8,
		/**
			The ratio of continuous-scrolling delta units to pixels scrolled. Increase
			this value to increase the distance scrolled when the pagination buttons
			are held.
		*/
		paginationScrollMultiplier: 8,
		/**
			When true, the scroll wheel moves spotlight focus up/down through the
			scroller when in 5-way mode. (In pointer mode, the scroll wheel always
			scrolls the viewport without modifying focus position.) When false, the
			scroll wheel works the same in 5-way mode as in pointer mode, where the
			wheel moves the position of the scroller viewport.
		*/
		scrollWheelMovesFocus: true
	},
	//* @protected
	handlers: {
		onSpotlightScrollUp:"spotlightWheel",
		onSpotlightScrollDown:"spotlightWheel",
		onSpotlightContainerEnter: "spotlightHello",
		onSpotlightFocus: "spotlightHello",
		onSpotlightContainerLeave: "spotlightGoodbye"
	},
	//* If true, scroll events are not allowed to propagate
	preventScrollPropagation: false,
	//* Default to moon.ScrollStrategy
	strategyKind: "moon.ScrollStrategy",
	/**
		Scrolls until _inControl_ is in view. If _inScrollFullPage_ is set, scrolls
		until the edge of _inControl_ is aligned with the edge of the visible scroll
		area. Optional third parameter to indicate whether or not it should animate
		the scroll. Defaults to animation unless it is set to false.
		If _setLastFocusedChild_ is true, scroller will set up _inControl_ to be the spotted child
		when scroller is spotted.
	*/
	scrollToControl: function(inControl, inScrollFullPage, animate, setLastFocusedChild) {
		if (setLastFocusedChild) {
			this.$.strategy.setLastFocusedChild(inControl);
		}
		this.$.strategy.animateToControl(inControl, inScrollFullPage, animate);
	},

	/**
		Accepts third optional paramater to indicate whether or not it should
		animate the scroll. Defaults to animation unless it is set to false.
	*/
	scrollTo: function (x, y, animate) {
		this.$.strategy.scrollTo(x, y, animate);
	},

	//* @protected
	bindings: [
		{from: ".scrollInterval",				to:".$.strategy.interval"},
		{from: ".scrollWheelMultiplier",		to:".$.strategy.scrollWheelMultiplier"},
		{from: ".scrollWheelPageMultiplier",	to:".$.strategy.scrollWheelPageMultiplier"},
		{from: ".paginationPageMultiplier",		to:".$.strategy.paginationPageMultiplier"},
		{from: ".paginationScrollMultiplier",	to:".$.strategy.paginationScrollMultiplier"}
	],
	create: function() {
		this.inherited(arguments);
		this.spotlightPagingControlsChanged();
		this.scrollWheelMovesFocusChanged();
	},
	spotlightPagingControlsChanged: function() {
		this.$.strategy.set("spotlightPagingControls", this.spotlightPagingControls);
	},
	scrollWheelMovesFocusChanged: function() {
		if (!this.scrollWheelMovesFocus) {
			this.setUseMouseWheel(true);
		}
	},
	spotlightWheel: function(inSender, inEvent) {
		if (this.scrollWheelMovesFocus) {
			if (!enyo.Spotlight.getPointerMode()) {
				var curr = enyo.Spotlight.getCurrent();
				if (curr && curr.isDescendantOf(this)) {
					var dir = inEvent.type == "onSpotlightScrollUp" ? "onSpotlightUp" : "onSpotlightDown";
					this._spotlightModal = this.spotlightModal;
					this.spotlightModal = true;	// Trap focus inside scroller while wheeling
					enyo.Spotlight.Util.dispatchEvent(dir, {type: dir}, curr);
					this.spotlightModal = this._spotlightModal;
					return true;
				}
			}
		}
	},
	// When scroller is entered or one of its children is focused
	// in 5-way mode, make sure that we're showing the scroll columns
	spotlightHello: function(inSender, inEvent) {
		if (this.$.strategy.showHideScrollColumns) {
			this.$.strategy.showHideScrollColumns(true);
		}
	},
	// When 5-way focus leaves scroller, hide the scroll columns
	spotlightGoodbye: function(inSender, inEvent) {
		if (inEvent.originator === this && this.$.strategy.showHideScrollColumns) {
			this.$.strategy.showHideScrollColumns(false);
		}
	},
	previewDomEvent: function(inEvent) {
		if (this.scrollWheelMovesFocus) {
			if (inEvent.type == "mousewheel") {
				this.setUseMouseWheel(enyo.Spotlight.getPointerMode());
			}
		}
	}
});

// On touch platforms, revert to using Enyo scroller, which picks an appropriate
// scroll strategy for the given platform
if (enyo.platform.touch) {
	moon.Scroller = enyo.Scroller;
}
<|endoftext|>


<commit_before>'use strict';

function draw(){
    buffer.clearRect(
      0,
      0,
      width,
      height
    );

    // Draw ground grass.
    buffer.fillStyle = '#131';
    buffer.fillRect(
      0,
      floor_position,
      width,
      y
    );

    buffer.fillStyle = split_state[0]
      ? '#323232'
      : '#646464';

    // Precalculate left wall split position.
    var precalc = (splits[0][0] * (1 / splits[0][2])) + x;

    // Draw left wall extra bit.
    if(player_position > 0){
        buffer.fillRect(
          0,
          0,
          1 + (player_position >= precalc ? precalc : player_position),
          height
        );
    }

    // Draw left wall closer than split.
    if(player_position < precalc){
        buffer.beginPath();
        buffer.moveTo(
          player_position,
          y - x
        );
        buffer.lineTo(
          precalc,
          (splits[0][1] * (1 / splits[0][2])) + y
        );
        buffer.lineTo(
          precalc,
          (splits[1][1] * (1 / splits[1][2])) + y
        );
        buffer.lineTo(
          player_position,
          y + x
        );
        buffer.closePath();
        buffer.fill();
    }

    // Draw left wall further than split.
    buffer.fillStyle = split_state[0]
      ? '#646464'
      : '#323232';
    buffer.beginPath();
    buffer.moveTo(
      precalc,
      (splits[0][1] * (1 / splits[0][2])) + y
    );
    buffer.lineTo(
      x,
      y
    );
    buffer.lineTo(
      precalc,
      (splits[1][1] * (1 / splits[1][2])) + y
    );
    buffer.closePath();
    buffer.fill();

    buffer.fillStyle = split_state[0]
      ? '#646464'
      : '#323232';

    // Precalculate right wall split position.
    precalc = (splits[2][0] * (1 / splits[2][2])) + x;

    // Draw right wall extra bit.
    if(player_position < 0){
        buffer.fillRect(
          width + player_position < precalc
            ? precalc - 1
            : width + player_position - 1,
          0,
          -player_position,
          height
        );
    }

    // Draw right wall closer than split.
    if(width + player_position > precalc){
        buffer.beginPath();
        buffer.moveTo(
          width + player_position,
          y - x
        );
        buffer.lineTo(
          precalc,
          (splits[2][1] * (1 / splits[2][2])) + y
        );
        buffer.lineTo(
          precalc,
          (splits[3][1] * (1 / splits[3][2])) + y
        );
        buffer.lineTo(
          width + player_position,
          y + x
        );
        buffer.closePath();
        buffer.fill();
    }

    // Draw right wall further than split.
    buffer.fillStyle = split_state[0]
      ? '#323232'
      : '#646464';

    buffer.beginPath();
    buffer.moveTo(
      precalc,
      (splits[2][1] * (1 / splits[2][2])) + y
    );
    buffer.lineTo(
      x,
      y
    );
    buffer.lineTo(
      precalc,
      (splits[3][1] * (1 / splits[3][2])) + y
    );
    buffer.closePath();
    buffer.fill();

    // If frame counter is enabled, draw current frame count.
    if(settings['frame-counter']){
        buffer.fillStyle = '#fff';
        buffer.fillText(
          frame_counter,
          5,
          25
        );
        buffer.fillText(
          best,
          5,
          50
        );
    }

    canvas.clearRect(
      0,
      0,
      width,
      height
    );
    canvas.drawImage(
      document.getElementById('buffer'),
      0,
      0
    );

    animationFrame = window.requestAnimationFrame(draw);
}

function logic(){
    frame_counter += 1;

    // Get player movement.
    var player_dx = 0;
    if(key_left){
        player_dx += x / 20;
    }
    if(key_right){
        player_dx -= x / 20;
    }

    // Update player position.
    player_position += player_dx;

    // Make sure player stays in bounds.
    if(player_position > x / 1.5){
        player_position = x / 1.5;

    }else if(player_position < -x / 1.5){
        player_position = -x / 1.5;
    }

    // Move player forward by moving splits closer.
    for(var id in splits){
        splits[id][2] -= .05;

        // If splits reach player, reset splits.
        if(splits[id][2] < 0){
            splits[id][2] = 25;
            split_state[1] = true;
        }
    }

    // If it is time to reset split.
    if(split_state[1]){
        // Reset player position to start of new section.
        player_position = 0;

        // Switch split.
        split_state[1] = false;
        split_state[0] = !split_state[0];
    }
}

function reset(){
    if(!window.confirm('Reset settings?')){
        return;
    }

    document.getElementById('audio-volume').value = 1;
    document.getElementById('frame-counter').checked = true;
    document.getElementById('movement-keys').value = 'AD';
    document.getElementById('ms-per-frame').value = 25;
    document.getElementById('restart-key').value = 'H';

    save();
}

function reset_best(){
    if(!window.confirm('Reset best?')){
        return;
    }

    best = 0;
    frame_counter = 0;
    update_best();
    setmode(
      0,
      true
    );
}

function resize(){
    if(mode <= 0){
        return;
    }

    height = window.innerHeight;
    document.getElementById('buffer').height = height;
    document.getElementById('canvas').height = height;
    y = height / 2;

    floor_position = y * (mode - 1);

    width = window.innerWidth;
    document.getElementById('buffer').width = width;
    document.getElementById('canvas').width = width;
    x = width / 2;

    buffer.font = '23pt sans-serif';
}

// Save settings into window.localStorage if they differ from default.
function save(){
    var audio_volume = document.getElementById('audio-volume').value;
    if(audio_volume == 1){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-audio-volume');
        settings['audio-volume'] = 1;

    }else{
        settings['audio-volume'] = parseFloat(audio_volume);
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-audio-volume',
          settings['audio-volume']
        );
    }

    if(document.getElementById('frame-counter').checked){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-frame-counter');
        settings['frame-counter'] = true;

    }else{
        settings['frame-counter'] = false;
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-frame-counter',
          1
        );
    }

    var movement_keys = document.getElementById('movement-keys').value;
    if(movement_keys === 'AD'){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-movement-keys');
        settings['movement-keys'] = 'AD';

    }else{
        settings['movement-keys'] = movement_keys;
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-movement-keys',
          settings['movement-keys']
        );
    }

    var ms_per_frame = document.getElementById('ms-per-frame').value;
    if(ms_per_frame == 25
      || isNaN(ms_per_frame)
      || ms_per_frame < 1){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-ms-per-frame');
        settings['ms-per-frame'] = 25;

    }else{
        settings['ms-per-frame'] = parseInt(ms_per_frame);
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-ms-per-frame',
          settings['ms-per-frame']
        );
    }

    var restart_key = document.getElementById('restart-key').value;
    if(restart_key === 'H'){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-restart-key');
        settings['restart-key'] = 'H';

    }else{
        settings['restart-key'] = restart_key;
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-restart-key',
          settings['restart-key']
        );
    }
}

function setmode(newmode, newgame){
    window.cancelAnimationFrame(animationFrame);
    window.clearInterval(interval);

    split_state = [
      false,
      false,
    ];

    mode = newmode;

    // New game mode.
    if(mode > 0){
        splits = [
          [-50, -50, 25],
          [-50, 50, 25],
          [50, -50, 25],
          [50, 50, 25],
        ];

        frame_counter = 0;
        player_position = 0;

        if(newgame){
            save();

            document.body.innerHTML =
              '<canvas id=canvas></canvas><canvas id=buffer></canvas>';

            var contextAttributes = {
              'alpha': false,
            };
            buffer = document.getElementById('buffer').getContext(
              '2d',
              contextAttributes
            );
            canvas = document.getElementById('canvas').getContext(
              '2d',
              contextAttributes
            );

            resize();
        }

        animationFrame = window.requestAnimationFrame(draw);
        interval = window.setInterval(
          'logic()',
          settings['ms-per-frame']
        );

        return;
    }

    // Main menu mode.
    buffer = 0;
    canvas = 0;

    document.body.innerHTML = '

Cling to the Ground
Walled Corridor

Best: '
      + best
      + '
Reset Best

<input disabled value=ESC>Main Menu
<input id=movement-keys maxlength=2 value='
      + settings['movement-keys'] + '>Move ←→
<input id=restart-key maxlength=1 value='
      + settings['restart-key'] + '>Restart

<input id=audio-volume max=1 min=0 step=0.01 type=range value='
      + settings['audio-volume'] + '>Audio
<input id=ms-per-frame value='
      + settings['ms-per-frame'] + '>ms/Frame
<input '
      + (settings['frame-counter'] ? 'checked ' : '') + 'id=frame-counter type=checkbox>Frame Counter
Reset Settings

';
}

function update_best(){
    if(frame_counter > best){
        best = frame_counter;
    }

    if(best > 0){
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-best',
          best
        );

    }else{
        window.localStorage.removeItem('FractalRunner-2D3D.htm-best');
    }
}

var animationFrame = 0;
var best = parseInt(window.localStorage.getItem('FractalRunner-2D3D.htm-best')) || 0;
var buffer = 0;
var canvas = 0;
var floor_position = 0;
var frame_counter = 0;
var height = 0;
var interval = 0;
var key_left = false;
var key_right = false;
var mode = 0;
var player_position = 0;
var settings = {
  'audio-volume': window.localStorage.getItem('FractalRunner-2D3D.htm-audio-volume') !== null
    ? parseFloat(window.localStorage.getItem('FractalRunner-2D3D.htm-audio-volume'))
    : 1,
  'frame-counter': window.localStorage.getItem('FractalRunner-2D3D.htm-frame-counter') === null,
  'movement-keys': window.localStorage.getItem('FractalRunner-2D3D.htm-movement-keys') || 'AD',
  'ms-per-frame': parseFloat(window.localStorage.getItem('FractalRunner-2D3D.htm-ms-per-frame')) || 25,
  'restart-key': window.localStorage.getItem('FractalRunner-2D3D.htm-restart-key') || 'H',
};
var split_state = [];
var splits = [];
var width = 0;
var x = 0;
var y = 0;

window.onkeydown = function(e){
    if(mode <= 0){
        return;
    }

    var key = e.keyCode || e.which;

    // ESC: return to main menu.
    if(key === 27){
        update_best();
        setmode(
          0,
          true
        );
        return;
    }

    key = String.fromCharCode(key);

    if(key === settings['movement-keys'][0]){
        key_left = true;

    }else if(key === settings['movement-keys'][1]){
        key_right = true;

    }else if(key === settings['restart-key']){
        update_best();
        setmode(
          mode,
          false
        );
    }
};

window.onkeyup = function(e){
    var key = String.fromCharCode(e.keyCode || e.which);

    if(key === settings['movement-keys'][0]){
        key_left = false;

    }else if(key === settings['movement-keys'][1]){
        key_right = false;
    }
};

window.onload = function(e){
    setmode(
      0,
      true
    );
};

window.onresize = resize;
<commit_msg>simplified reset() and save()<commit_after>'use strict';

function draw(){
    buffer.clearRect(
      0,
      0,
      width,
      height
    );

    // Draw ground grass.
    buffer.fillStyle = '#131';
    buffer.fillRect(
      0,
      floor_position,
      width,
      y
    );

    buffer.fillStyle = split_state[0]
      ? '#323232'
      : '#646464';

    // Precalculate left wall split position.
    var precalc = (splits[0][0] * (1 / splits[0][2])) + x;

    // Draw left wall extra bit.
    if(player_position > 0){
        buffer.fillRect(
          0,
          0,
          1 + (player_position >= precalc ? precalc : player_position),
          height
        );
    }

    // Draw left wall closer than split.
    if(player_position < precalc){
        buffer.beginPath();
        buffer.moveTo(
          player_position,
          y - x
        );
        buffer.lineTo(
          precalc,
          (splits[0][1] * (1 / splits[0][2])) + y
        );
        buffer.lineTo(
          precalc,
          (splits[1][1] * (1 / splits[1][2])) + y
        );
        buffer.lineTo(
          player_position,
          y + x
        );
        buffer.closePath();
        buffer.fill();
    }

    // Draw left wall further than split.
    buffer.fillStyle = split_state[0]
      ? '#646464'
      : '#323232';
    buffer.beginPath();
    buffer.moveTo(
      precalc,
      (splits[0][1] * (1 / splits[0][2])) + y
    );
    buffer.lineTo(
      x,
      y
    );
    buffer.lineTo(
      precalc,
      (splits[1][1] * (1 / splits[1][2])) + y
    );
    buffer.closePath();
    buffer.fill();

    buffer.fillStyle = split_state[0]
      ? '#646464'
      : '#323232';

    // Precalculate right wall split position.
    precalc = (splits[2][0] * (1 / splits[2][2])) + x;

    // Draw right wall extra bit.
    if(player_position < 0){
        buffer.fillRect(
          width + player_position < precalc
            ? precalc - 1
            : width + player_position - 1,
          0,
          -player_position,
          height
        );
    }

    // Draw right wall closer than split.
    if(width + player_position > precalc){
        buffer.beginPath();
        buffer.moveTo(
          width + player_position,
          y - x
        );
        buffer.lineTo(
          precalc,
          (splits[2][1] * (1 / splits[2][2])) + y
        );
        buffer.lineTo(
          precalc,
          (splits[3][1] * (1 / splits[3][2])) + y
        );
        buffer.lineTo(
          width + player_position,
          y + x
        );
        buffer.closePath();
        buffer.fill();
    }

    // Draw right wall further than split.
    buffer.fillStyle = split_state[0]
      ? '#323232'
      : '#646464';

    buffer.beginPath();
    buffer.moveTo(
      precalc,
      (splits[2][1] * (1 / splits[2][2])) + y
    );
    buffer.lineTo(
      x,
      y
    );
    buffer.lineTo(
      precalc,
      (splits[3][1] * (1 / splits[3][2])) + y
    );
    buffer.closePath();
    buffer.fill();

    // If frame counter is enabled, draw current frame count.
    if(settings['frame-counter']){
        buffer.fillStyle = '#fff';
        buffer.fillText(
          frame_counter,
          5,
          25
        );
        buffer.fillText(
          best,
          5,
          50
        );
    }

    canvas.clearRect(
      0,
      0,
      width,
      height
    );
    canvas.drawImage(
      document.getElementById('buffer'),
      0,
      0
    );

    animationFrame = window.requestAnimationFrame(draw);
}

function logic(){
    frame_counter += 1;

    // Get player movement.
    var player_dx = 0;
    if(key_left){
        player_dx += x / 20;
    }
    if(key_right){
        player_dx -= x / 20;
    }

    // Update player position.
    player_position += player_dx;

    // Make sure player stays in bounds.
    if(player_position > x / 1.5){
        player_position = x / 1.5;

    }else if(player_position < -x / 1.5){
        player_position = -x / 1.5;
    }

    // Move player forward by moving splits closer.
    for(var id in splits){
        splits[id][2] -= .05;

        // If splits reach player, reset splits.
        if(splits[id][2] < 0){
            splits[id][2] = 25;
            split_state[1] = true;
        }
    }

    // If it is time to reset split.
    if(split_state[1]){
        // Reset player position to start of new section.
        player_position = 0;

        // Switch split.
        split_state[1] = false;
        split_state[0] = !split_state[0];
    }
}

function reset(){
    if(!window.confirm('Reset settings?')){
        return;
    }

    var ids = {
      'audio-volume': 1,
      'movement-keys': 'AD',
      'ms-per-frame': 25,
      'restart-key': 'H',
    };
    for(var id in ids){
        document.getElementById(id).value = ids[id];
    }
    document.getElementById('frame-counter').checked = true;

    save();
}

function reset_best(){
    if(!window.confirm('Reset best?')){
        return;
    }

    best = 0;
    frame_counter = 0;
    update_best();
    setmode(
      0,
      true
    );
}

function resize(){
    if(mode <= 0){
        return;
    }

    height = window.innerHeight;
    document.getElementById('buffer').height = height;
    document.getElementById('canvas').height = height;
    y = height / 2;

    floor_position = y * (mode - 1);

    width = window.innerWidth;
    document.getElementById('buffer').width = width;
    document.getElementById('canvas').width = width;
    x = width / 2;

    buffer.font = '23pt sans-serif';
}

// Save settings into window.localStorage if they differ from default.
function save(){
    var audio_volume = document.getElementById('audio-volume').value;
    if(audio_volume == 1){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-audio-volume');
        settings['audio-volume'] = 1;

    }else{
        settings['audio-volume'] = parseFloat(audio_volume);
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-audio-volume',
          settings['audio-volume']
        );
    }

    if(document.getElementById('frame-counter').checked){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-frame-counter');
        settings['frame-counter'] = true;

    }else{
        settings['frame-counter'] = false;
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-frame-counter',
          1
        );
    }

    var ids = {
      'movement-keys': 'AD',
      'restart-key': 'H',
    };
    for(var id in ids){
        var value = document.getElementById(id).value;
        if(value === ids[id]){
            window.localStorage.removeItem('FractalRunner-2D3D.htm-' + id);
            settings[id] = ids[id];

        }else{
            settings[id] = value;
            window.localStorage.setItem(
              'FractalRunner-2D3D.htm-' + id,
              value
            );
        }
    }

    var ms_per_frame = document.getElementById('ms-per-frame').value;
    if(ms_per_frame == 25
      || isNaN(ms_per_frame)
      || ms_per_frame < 1){
        window.localStorage.removeItem('FractalRunner-2D3D.htm-ms-per-frame');
        settings['ms-per-frame'] = 25;

    }else{
        settings['ms-per-frame'] = parseInt(ms_per_frame);
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-ms-per-frame',
          settings['ms-per-frame']
        );
    }
}

function setmode(newmode, newgame){
    window.cancelAnimationFrame(animationFrame);
    window.clearInterval(interval);

    split_state = [
      false,
      false,
    ];

    mode = newmode;

    // New game mode.
    if(mode > 0){
        splits = [
          [-50, -50, 25],
          [-50, 50, 25],
          [50, -50, 25],
          [50, 50, 25],
        ];

        frame_counter = 0;
        player_position = 0;

        if(newgame){
            save();

            document.body.innerHTML =
              '<canvas id=canvas></canvas><canvas id=buffer></canvas>';

            var contextAttributes = {
              'alpha': false,
            };
            buffer = document.getElementById('buffer').getContext(
              '2d',
              contextAttributes
            );
            canvas = document.getElementById('canvas').getContext(
              '2d',
              contextAttributes
            );

            resize();
        }

        animationFrame = window.requestAnimationFrame(draw);
        interval = window.setInterval(
          'logic()',
          settings['ms-per-frame']
        );

        return;
    }

    // Main menu mode.
    buffer = 0;
    canvas = 0;

    document.body.innerHTML = '

Cling to the Ground
Walled Corridor

Best: '
      + best
      + '
Reset Best

<input disabled value=ESC>Main Menu
<input id=movement-keys maxlength=2 value='
      + settings['movement-keys'] + '>Move ←→
<input id=restart-key maxlength=1 value='
      + settings['restart-key'] + '>Restart

<input id=audio-volume max=1 min=0 step=0.01 type=range value='
      + settings['audio-volume'] + '>Audio
<input id=ms-per-frame value='
      + settings['ms-per-frame'] + '>ms/Frame
<input '
      + (settings['frame-counter'] ? 'checked ' : '') + 'id=frame-counter type=checkbox>Frame Counter
Reset Settings

';
}

function update_best(){
    if(frame_counter > best){
        best = frame_counter;
    }

    if(best > 0){
        window.localStorage.setItem(
          'FractalRunner-2D3D.htm-best',
          best
        );

    }else{
        window.localStorage.removeItem('FractalRunner-2D3D.htm-best');
    }
}

var animationFrame = 0;
var best = parseInt(window.localStorage.getItem('FractalRunner-2D3D.htm-best')) || 0;
var buffer = 0;
var canvas = 0;
var floor_position = 0;
var frame_counter = 0;
var height = 0;
var interval = 0;
var key_left = false;
var key_right = false;
var mode = 0;
var player_position = 0;
var settings = {
  'audio-volume': window.localStorage.getItem('FractalRunner-2D3D.htm-audio-volume') !== null
    ? parseFloat(window.localStorage.getItem('FractalRunner-2D3D.htm-audio-volume'))
    : 1,
  'frame-counter': window.localStorage.getItem('FractalRunner-2D3D.htm-frame-counter') === null,
  'movement-keys': window.localStorage.getItem('FractalRunner-2D3D.htm-movement-keys') || 'AD',
  'ms-per-frame': parseFloat(window.localStorage.getItem('FractalRunner-2D3D.htm-ms-per-frame')) || 25,
  'restart-key': window.localStorage.getItem('FractalRunner-2D3D.htm-restart-key') || 'H',
};
var split_state = [];
var splits = [];
var width = 0;
var x = 0;
var y = 0;

window.onkeydown = function(e){
    if(mode <= 0){
        return;
    }

    var key = e.keyCode || e.which;

    // ESC: return to main menu.
    if(key === 27){
        update_best();
        setmode(
          0,
          true
        );
        return;
    }

    key = String.fromCharCode(key);

    if(key === settings['movement-keys'][0]){
        key_left = true;

    }else if(key === settings['movement-keys'][1]){
        key_right = true;

    }else if(key === settings['restart-key']){
        update_best();
        setmode(
          mode,
          false
        );
    }
};

window.onkeyup = function(e){
    var key = String.fromCharCode(e.keyCode || e.which);

    if(key === settings['movement-keys'][0]){
        key_left = false;

    }else if(key === settings['movement-keys'][1]){
        key_right = false;
    }
};

window.onload = function(e){
    setmode(
      0,
      true
    );
};

window.onresize = resize;
<|endoftext|>


<commit_before>const path = require('path')
const fs = require('fs-extra')
import cookie from 'cookie'

//

import koaStatic from 'koa-static'
import convert from 'koa-convert'

//

import i18nRegister from '../../i18n/register/isomorphic.server'
import i18nOnServerRender from '../../i18n/onServerRender'
import { changeLocaleQueryKey } from '../../defaults/defines'

//

import { CHANGE_LANGUAGE, TELL_CLIENT_URL, SYNC_COOKIE } from '../action-types'
import kootClient from '../client/run'
import ReactIsomorphic from '../ReactIsomorphic'
const getDistPath = require('../../utils/get-dist-path')
const getDirDistPublic = require('../../libs/get-dir-dist-public')

import middlewareRouterDev from './middlewares/router-dev'

const cache = {}

const getLocalesDefault = () => {
    if (__DEV__) {
        const locales = JSON.parse(process.env.KOOT_I18N_LOCALES)
        return locales.map(l => ([
            l[0],
            fs.readJsonSync(l[2], 'utf-8'),
            l[2]
        ]))
    }
    return JSON.parse(process.env.KOOT_I18N_LOCALES)
}
const doI18nRegister = (
    locales = getLocalesDefault(),
    i18nType = JSON.parse(process.env.KOOT_I18N_TYPE) || false,
) => {
    const availableLocales = []
    const localesObj = {}
    locales.forEach(arr => {
        const [localeId, localeObj] = arr
        availableLocales.push(localeId)
        localesObj[localeId] = localeObj
    })
    // 服务器端注册多语言
    i18nRegister({
        localeIds: availableLocales,
        locales: localesObj,
        type: i18nType,
    })
}

/**
 * 为 Koa server 进行调整、添加中间件等操作，以支持 React 同构
 * @async
 * @param {Object} app Koa server/app 实例
 * @param {Object} options
 * @param {String} [options.template] HTML 模板内容
 * @param {Boolean|Array} [options.i18n] i18n 设置项
 * @param {Boolean|String} [options.i18nType] i18n 类型
 * @param {Object} [options.locales] i18n 语言包
 * @param {Object} [options.router] 路由设置
 * @param {Object} [options.redux] redux 设置
 * @param {Object} [options.client] 客户端/浏览器端设置项
 * @param {Object} [options.server] 服务器端设置项
 * @returns {Object} 调整后的 Koa server/app 实例
 */
export default async (app, {
    // name,
    template,
    i18n = JSON.parse(process.env.KOOT_I18N) || false,
    i18nType = JSON.parse(process.env.KOOT_I18N_TYPE) || false,
    locales = getLocalesDefault(),
    router,
    redux,
    // store,
    client,
    server,
}) => {

    // if (__DEV__) console.log('\r\nServer initializing...')
    // else
    console.log(`\r\n  \x1b[93m[koot/server]\x1b[0m initializing...`)

    // ============================================================================
    // 相关检查、赋值
    // ============================================================================
    const {
        inject,
        before,
        after,
        renderCache,
    } = server
    const onRender = server.render || server.onRender

    // 处理 template
    if (typeof cache.template === 'string') {
        template = cache.template
    } else {
        if (typeof process.env.KOOT_HTML_TEMPLATE === 'string')
            template = process.env.KOOT_HTML_TEMPLATE

        if (typeof template !== 'string')
            throw new Error('Error: "template" type check fail!')

        // if (template.substr(0, 2) === './') {
        //     // template = require(`raw-loader?` + path.resolve(
        //     //     getCwd(), template
        //     // ))
        //     template = fs.readFileSync(path.resolve(
        //         getCwd(), template
        //     ), 'utf-8')
        // }

        cache.template = template
        // process.env.KOOT_HTML_TEMPLATE = template
    }

    if (typeof inject !== 'object')
        throw new Error('Error: "server.inject" type check fail!')

    // ============================================================================
    // 载入目录、相关配置、自定模块等
    // ============================================================================
    // if (__DEV__) console.log('├─ client code initializing...')
    if (__DEV__) console.log(`  \x1b[93m[koot/server]\x1b[0m client code initializing...`)
    const reactApp = await kootClient({
        i18n,
        router,
        redux,
        // store,
        client
    })
    // if (__DEV__) console.log('├─ client code inited')
    if (__DEV__) console.log(`  \x1b[93m[koot/server]\x1b[0m client code inited`)

    // ============================================================================
    // 对应client的server端处理
    // ============================================================================

    if (i18n) doI18nRegister(locales, i18nType)

    // ============================================================================
    // 创建KOA实例
    // ============================================================================

    if (__DEV__)
        console.log(
            `\n\n`
            + `\x1b[36m⚑\x1b[0m `
            + `\x1b[93m[koot/server]\x1b[0m `
            + `callback: \x1b[32m${'before'}\x1b[0m`
            + `(app)`
            + `\n`
        )
    if (typeof before === 'function') {
        await before(app)
    }

    /* 静态目录,用于外界访问打包好的静态文件js、css等 */
    app.use(convert(koaStatic(
        getDirDistPublic(getDistPath()),
        {
            maxage: 0,
            hidden: true,
            index: 'index.html',
            defer: false,
            gzip: true,
            extensions: false
        }
    )))

    // ============================================================================
    // Webpack 打包配置
    // ============================================================================
    // await app.use(async (ctx, next) => {
    //     if (!__DEV__) __webpack_public_path__ = process.env.WEBPACK_SERVER_PUBLIC_PATH || ''
    //     await next()
    // })

    // ============================================================================
    // 同构配置
    // ============================================================================
    reactApp.isomorphic = new ReactIsomorphic()

    const isomorphic = reactApp.isomorphic.createKoaMiddleware({
        reactApp,

        // react-router 配置对象
        routes: reactApp.react.router.get(),

        // redux store 对象
        configStore: typeof redux.store === 'undefined' ? reactApp.createConfigureStoreFactory() : undefined,
        store: typeof redux.store === 'undefined' ? undefined : redux.store,

        // HTML基础模板
        template,

        // 对HTML基础模板的自定义注入
        // 例如：<script>//inject_critical</script>  替换为 critical
        inject,

        renderCache,

        onServerRender: async (obj) => {
            if (__DEV__) console.log(' ')

            let { ctx, store } = obj

            // 告诉前端，当前的url是啥
            store.dispatch({ type: TELL_CLIENT_URL, data: ctx.origin })

            // 把http请求带来的cookie同步到ssr的初始化redux state里
            // server.cookie 获取
            // 配置信息在koot.config.js里
            // redux.syncCookie = ['token', 'sid'] | 'token' | false
            if (redux.syncCookie) {
                let cookies = redux.syncCookie

                // 结构统一
                if (typeof cookies === 'string')
                    cookies = [cookies]

                // 获取需要的cookie值
                const data = {}
                cookies.forEach(c => {
                    data[c] = ctx.cookies.get(c)
                })

                // 同步到state中
                store.dispatch({ type: SYNC_COOKIE, data })
            }

            if (i18n) {
                let lang = (() => {

                    // 先查看URL参数是否有语音设置
                    let lang = ctx.query[changeLocaleQueryKey]

                    // 如果没有，检查cookie
                    const cookies = cookie.parse(ctx.request.header.cookie || '')
                    if (!lang && cookies[process.env.KOOT_I18N_COOKIE_KEY] && cookies[process.env.KOOT_I18N_COOKIE_KEY] !== 'null')
                        lang = cookies[process.env.KOOT_I18N_COOKIE_KEY]

                    // 如果没有，再看header里是否有语言设置
                    if (!lang)
                        lang = ctx.header['accept-language']

                    // 如没有，再用默认
                    if (!lang)
                        lang = 'en'

                    return lang
                })()

                if (__DEV__) doI18nRegister()

                store.dispatch({ type: CHANGE_LANGUAGE, data: lang })
                i18nOnServerRender(obj)
            }

            if (__DEV__)
                console.log(
                    `\n\n`
                    + `\x1b[36m⚑\x1b[0m `
                    + `\x1b[93m[koot/server]\x1b[0m `
                    + `callback: \x1b[32m${'onRender'}\x1b[0m`
                    + `({ ctx, store })`
                    + `\n`
                )

            if (typeof onRender === 'function')
                await onRender(obj)
        }
    })

    app.use(isomorphic)

    if (__DEV__) {
        app.use(middlewareRouterDev)
        console.log(
            `\n\n`
            + `\x1b[36m⚑\x1b[0m `
            + `\x1b[93m[koot/server]\x1b[0m `
            + `callback: \x1b[32m${'after'}\x1b[0m`
            + `(app)`
            + `\n`
        )
    }
    if (typeof after === 'function') {
        await after(app)
    }

    // if (__DEV__) console.log('└─ ✔ Server inited.\r\n')
    // else
    console.log(`  \x1b[93m[koot/server]\x1b[0m init \x1b[32m${'OK'}\x1b[0m!`)

    return app
}
<commit_msg>[koot] dev mode: use dev router middleware at `before`<commit_after>const path = require('path')
const fs = require('fs-extra')
import cookie from 'cookie'

//

import koaStatic from 'koa-static'
import convert from 'koa-convert'

//

import i18nRegister from '../../i18n/register/isomorphic.server'
import i18nOnServerRender from '../../i18n/onServerRender'
import { changeLocaleQueryKey } from '../../defaults/defines'

//

import { CHANGE_LANGUAGE, TELL_CLIENT_URL, SYNC_COOKIE } from '../action-types'
import kootClient from '../client/run'
import ReactIsomorphic from '../ReactIsomorphic'
const getDistPath = require('../../utils/get-dist-path')
const getDirDistPublic = require('../../libs/get-dir-dist-public')

import middlewareRouterDev from './middlewares/router-dev'

const cache = {}

const getLocalesDefault = () => {
    if (__DEV__) {
        const locales = JSON.parse(process.env.KOOT_I18N_LOCALES)
        return locales.map(l => ([
            l[0],
            fs.readJsonSync(l[2], 'utf-8'),
            l[2]
        ]))
    }
    return JSON.parse(process.env.KOOT_I18N_LOCALES)
}
const doI18nRegister = (
    locales = getLocalesDefault(),
    i18nType = JSON.parse(process.env.KOOT_I18N_TYPE) || false,
) => {
    const availableLocales = []
    const localesObj = {}
    locales.forEach(arr => {
        const [localeId, localeObj] = arr
        availableLocales.push(localeId)
        localesObj[localeId] = localeObj
    })
    // 服务器端注册多语言
    i18nRegister({
        localeIds: availableLocales,
        locales: localesObj,
        type: i18nType,
    })
}

/**
 * 为 Koa server 进行调整、添加中间件等操作，以支持 React 同构
 * @async
 * @param {Object} app Koa server/app 实例
 * @param {Object} options
 * @param {String} [options.template] HTML 模板内容
 * @param {Boolean|Array} [options.i18n] i18n 设置项
 * @param {Boolean|String} [options.i18nType] i18n 类型
 * @param {Object} [options.locales] i18n 语言包
 * @param {Object} [options.router] 路由设置
 * @param {Object} [options.redux] redux 设置
 * @param {Object} [options.client] 客户端/浏览器端设置项
 * @param {Object} [options.server] 服务器端设置项
 * @returns {Object} 调整后的 Koa server/app 实例
 */
export default async (app, {
    // name,
    template,
    i18n = JSON.parse(process.env.KOOT_I18N) || false,
    i18nType = JSON.parse(process.env.KOOT_I18N_TYPE) || false,
    locales = getLocalesDefault(),
    router,
    redux,
    // store,
    client,
    server,
}) => {

    // if (__DEV__) console.log('\r\nServer initializing...')
    // else
    console.log(`\r\n  \x1b[93m[koot/server]\x1b[0m initializing...`)

    // ============================================================================
    // 相关检查、赋值
    // ============================================================================
    const {
        inject,
        before,
        after,
        renderCache,
    } = server
    const onRender = server.render || server.onRender

    // 处理 template
    if (typeof cache.template === 'string') {
        template = cache.template
    } else {
        if (typeof process.env.KOOT_HTML_TEMPLATE === 'string')
            template = process.env.KOOT_HTML_TEMPLATE

        if (typeof template !== 'string')
            throw new Error('Error: "template" type check fail!')

        // if (template.substr(0, 2) === './') {
        //     // template = require(`raw-loader?` + path.resolve(
        //     //     getCwd(), template
        //     // ))
        //     template = fs.readFileSync(path.resolve(
        //         getCwd(), template
        //     ), 'utf-8')
        // }

        cache.template = template
        // process.env.KOOT_HTML_TEMPLATE = template
    }

    if (typeof inject !== 'object')
        throw new Error('Error: "server.inject" type check fail!')

    // ============================================================================
    // 载入目录、相关配置、自定模块等
    // ============================================================================
    // if (__DEV__) console.log('├─ client code initializing...')
    if (__DEV__) console.log(`  \x1b[93m[koot/server]\x1b[0m client code initializing...`)
    const reactApp = await kootClient({
        i18n,
        router,
        redux,
        // store,
        client
    })
    // if (__DEV__) console.log('├─ client code inited')
    if (__DEV__) console.log(`  \x1b[93m[koot/server]\x1b[0m client code inited`)

    // ============================================================================
    // 对应client的server端处理
    // ============================================================================

    if (i18n) doI18nRegister(locales, i18nType)

    // ============================================================================
    // 创建KOA实例
    // ============================================================================

    if (__DEV__)
        console.log(
            `\n\n`
            + `\x1b[36m⚑\x1b[0m `
            + `\x1b[93m[koot/server]\x1b[0m `
            + `callback: \x1b[32m${'before'}\x1b[0m`
            + `(app)`
            + `\n`
        )
    if (typeof before === 'function') {
        await before(app)
    }

    /* 静态目录,用于外界访问打包好的静态文件js、css等 */
    if (__DEV__) {
        app.use(middlewareRouterDev)
    }
    app.use(convert(koaStatic(
        getDirDistPublic(getDistPath()),
        {
            maxage: 0,
            hidden: true,
            index: 'index.html',
            defer: false,
            gzip: true,
            extensions: false
        }
    )))

    // ============================================================================
    // Webpack 打包配置
    // ============================================================================
    // await app.use(async (ctx, next) => {
    //     if (!__DEV__) __webpack_public_path__ = process.env.WEBPACK_SERVER_PUBLIC_PATH || ''
    //     await next()
    // })

    // ============================================================================
    // 同构配置
    // ============================================================================
    reactApp.isomorphic = new ReactIsomorphic()

    const isomorphic = reactApp.isomorphic.createKoaMiddleware({
        reactApp,

        // react-router 配置对象
        routes: reactApp.react.router.get(),

        // redux store 对象
        configStore: typeof redux.store === 'undefined' ? reactApp.createConfigureStoreFactory() : undefined,
        store: typeof redux.store === 'undefined' ? undefined : redux.store,

        // HTML基础模板
        template,

        // 对HTML基础模板的自定义注入
        // 例如：<script>//inject_critical</script>  替换为 critical
        inject,

        renderCache,

        onServerRender: async (obj) => {
            if (__DEV__) console.log(' ')

            let { ctx, store } = obj

            // 告诉前端，当前的url是啥
            store.dispatch({ type: TELL_CLIENT_URL, data: ctx.origin })

            // 把http请求带来的cookie同步到ssr的初始化redux state里
            // server.cookie 获取
            // 配置信息在koot.config.js里
            // redux.syncCookie = ['token', 'sid'] | 'token' | false
            if (redux.syncCookie) {
                let cookies = redux.syncCookie

                // 结构统一
                if (typeof cookies === 'string')
                    cookies = [cookies]

                // 获取需要的cookie值
                const data = {}
                cookies.forEach(c => {
                    data[c] = ctx.cookies.get(c)
                })

                // 同步到state中
                store.dispatch({ type: SYNC_COOKIE, data })
            }

            if (i18n) {
                let lang = (() => {

                    // 先查看URL参数是否有语音设置
                    let lang = ctx.query[changeLocaleQueryKey]

                    // 如果没有，检查cookie
                    const cookies = cookie.parse(ctx.request.header.cookie || '')
                    if (!lang && cookies[process.env.KOOT_I18N_COOKIE_KEY] && cookies[process.env.KOOT_I18N_COOKIE_KEY] !== 'null')
                        lang = cookies[process.env.KOOT_I18N_COOKIE_KEY]

                    // 如果没有，再看header里是否有语言设置
                    if (!lang)
                        lang = ctx.header['accept-language']

                    // 如没有，再用默认
                    if (!lang)
                        lang = 'en'

                    return lang
                })()

                if (__DEV__) doI18nRegister()

                store.dispatch({ type: CHANGE_LANGUAGE, data: lang })
                i18nOnServerRender(obj)
            }

            if (__DEV__)
                console.log(
                    `\n\n`
                    + `\x1b[36m⚑\x1b[0m `
                    + `\x1b[93m[koot/server]\x1b[0m `
                    + `callback: \x1b[32m${'onRender'}\x1b[0m`
                    + `({ ctx, store })`
                    + `\n`
                )

            if (typeof onRender === 'function')
                await onRender(obj)
        }
    })

    app.use(isomorphic)

    if (__DEV__) {
        console.log(
            `\n\n`
            + `\x1b[36m⚑\x1b[0m `
            + `\x1b[93m[koot/server]\x1b[0m `
            + `callback: \x1b[32m${'after'}\x1b[0m`
            + `(app)`
            + `\n`
        )
    }
    if (typeof after === 'function') {
        await after(app)
    }

    // if (__DEV__) console.log('└─ ✔ Server inited.\r\n')
    // else
    console.log(`  \x1b[93m[koot/server]\x1b[0m init \x1b[32m${'OK'}\x1b[0m!`)

    return app
}
<|endoftext|>