<commit_before>/* eslint-env jquery */

import { getChar, print } from './io';
import { TxtFile, DirFile } from './fileobject';
import { ShellCommand, ShellCommandResult } from './shell-command';

export class Shell {
  constructor(fileStructure) {
    this.fileStructure = fileStructure;
    this.currentDir = fileStructure;
    this.user = 'guest';
    this.inputString = '';
    this.bashHistory = [];
    this.historyIndex = 0;
  }

  getPS1String() {
    return `<span class="user">${this.user}@www.carlegbert.com:</span>` +
      `<span class="path">${this.currentDir.fullPath}</span>$ `;
  }

  parseKeystroke(event) {
    if (event.which === 13) { // enter
      event.preventDefault();
      this.handleEnter(this.inputString);
    } else if (event.which === 8) { // backspace
      event.preventDefault();
      this.inputString = this.inputString.slice(0, (this.inputString.length - 1));
      $('#input').html(this.inputString.replace(/ /g, ' '));
    } else if (event.which === 38 && this.historyIndex > 0) { // up arrow
      event.preventDefault();
      this.historyIndex -= 1;
      this.inputString = this.bashHistory[this.historyIndex];
      $('#input').html(this.inputString);
    } else if (event.which === 40 && this.historyIndex < this.bashHistory.length) { // down
      event.preventDefault();
      this.historyIndex += 1;
      if (this.historyIndex === this.bashHistory.length) this.inputString = '';
      else this.inputString = this.bashHistory[this.historyIndex];
      $('#input').html(this.inputString);
    } else {
      const k = getChar(event);
      this.inputString += k;
      const kSpaceAdjusted = k === ' ' ? ' ' : k;
      $('#input').append(kSpaceAdjusted);
    }
  }

  handleEnter() {
    const shellCommand = new ShellCommand(this.inputString);
    if (!shellCommand.command) {
      print(this.getPS1String());
    } else {
      print(this.getPS1String() + this.inputString.replace(/ /g, ' '));
      print(this.executeCommand(shellCommand));
      this.bashHistory.push(this.inputString);
    }
    this.inputString = '';
    $('#input').html('');
    this.historyIndex = this.bashHistory.length;
  }

  executeCommand(shellCommand) {
    if (!Shell.validCommands().includes(shellCommand.command)) {
      return `${shellCommand.command}: command not found`;
    }
    const evalStr = `this.${shellCommand.command}(shellCommand)`;
    return eval(evalStr).getDefaultOutput();
  }

  findFile(dir, filepath, filetype) {
    if (filepath.length === 0 || filepath === '.') return dir;
    if (filepath[0] === '~') {
      return filepath.length === 1 ? this.fileStructure :
        this.findFile(this.fileStructure, filepath.slice(1), filetype);
    } else if (filepath[0] === '..') {
      return filepath.length === 1 ? dir.parentRef :
        this.findFile(dir.parentRef, filepath.slice(1), filetype);
    }

    let found = null;
    const typeToFind = filepath.length === 1 ? filetype : 'dir';
    dir.children.forEach((child) => {
      if ((filepath[0] === child.name) && (!typeToFind || typeToFind === child.filetype)) {
        found = child;
      }
    });
    return filepath.length === 1 ? found : this.findFile(found, filepath.slice(1), filetype);
  }

  newFile(filepath, filetype) {
    let dir;
    const pathStr = filepath.join('/');
    if (filepath.length === 1) dir = this.currentDir;
    else dir = this.findFile(this.currentDir, filepath.slice(0, -1), 'dir');
    if (!dir) return new ShellCommandResult(null, `${filepath.slice(0, -1).join('/')}: Directory not found`);
    let file;
    if (filetype === 'dir') file = new DirFile(filepath.slice(-1)[0], pathStr, filetype, dir);
    else file = new TxtFile(filepath.slice(-1)[0], pathStr, filetype, dir);
    dir.children.push(file);
    return new ShellCommandResult(null, null, file);
  }

  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   *                   shell commands                        *
   * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

  /* return list of valid commands that can be executed */
  static validCommands() {
    return [
      'clear',
      'pwd',
      'whoami',
      'cd',
      'ls',
      'cat',
      'touch',
      'mkdir',
      'echo',
    ];
  }

  clear() {
    $('#terminal-output').html('');
    return new ShellCommandResult();
  }

  pwd() {
    return new ShellCommandResult(this.currentDir.fullPath);
  }

  whoami() {
    return new ShellCommandResult(this.user);
  }

  cd(shellCommand) {
    if (shellCommand.args.length === 0) {
      this.currentDir = this.fileStructure;
      $('#PS1').html(this.getPS1String());
      return new ShellCommandResult();
    }
    const dir = this.findFile(this.currentDir, shellCommand.args[0].split('/'), 'dir');
    if (dir) {
      this.currentDir = dir;
      $('#PS1').html(this.getPS1String());
      return new ShellCommandResult();
    }
    return new ShellCommandResult(null, `${shellCommand.args[0]}: directory not found`);
  }

  ls(shellCommand) {
    const res = new ShellCommandResult([]);
    if (shellCommand.args.length === 0) {
      res.stdOut.push(Shell.lsHelper(this.currentDir));
    } else {
      shellCommand.args.forEach((arg) => {
        const dir = this.findFile(this.currentDir, arg.split('/'), 'dir');
        if (!dir) {
          res.stdErr.push(`ls: cannot access ${arg}: no such file or directory`);
        } else {
          let str = '';
          if (shellCommand.args.length > 1) str += `${arg}:`;
          str += Shell.lsHelper(dir);
          res.stdOut.push(str);
        }
      });
    }
    return res;
  }

  static lsHelper(dir) {
    let res = '
';
    dir.children.forEach((child) => {
      res += `<li class="ls ${child.filetype}">${child.name}</li>`;
    });
    res += '
';
    return res;
  }

  cat(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path);
      if (file && file.filetype === 'dir') {
        res.stdErr.push(`cat: ${file.name}: Is a directory`);
      } else if (file) {
        res.stdOut = res.stdOut.concat(file.contents);
      } else {
        res.stdErr.push(`cat: ${arg}: No such file or directory`);
      }
    });
    return res;
  }

  touch(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path, 'txt');
      if (file) file.lastModified = new Date();
      else {
        const newFileRes = this.newFile(path, 'txt');
        res.combine(newFileRes);
      }
    });
    return res;
  }

  mkdir(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path, 'dir');
      if (!file) {
        const newFileRes = this.newFile(path, 'dir');
        res.combine(newFileRes);
      }
    });
    return res;
  }

  echo(shellCommand) {
    const output = shellCommand.args.join(' ');
    return new ShellCommandResult(output);
  }

}
<commit_msg>slight reorg to handleEnter/executeCommand<commit_after>/* eslint-env jquery */

import { getChar, print } from './io';
import { TxtFile, DirFile } from './fileobject';
import { ShellCommand, ShellCommandResult } from './shell-command';

export class Shell {
  constructor(fileStructure) {
    this.fileStructure = fileStructure;
    this.currentDir = fileStructure;
    this.user = 'guest';
    this.inputString = '';
    this.bashHistory = [];
    this.historyIndex = 0;
  }

  getPS1String() {
    return `<span class="user">${this.user}@www.carlegbert.com:</span>` +
      `<span class="path">${this.currentDir.fullPath}</span>$ `;
  }

  parseKeystroke(event) {
    if (event.which === 13) { // enter
      event.preventDefault();
      this.handleEnter();
    } else if (event.which === 8) { // backspace
      event.preventDefault();
      this.inputString = this.inputString.slice(0, (this.inputString.length - 1));
      $('#input').html(this.inputString.replace(/ /g, ' '));
    } else if (event.which === 38 && this.historyIndex > 0) { // up arrow
      event.preventDefault();
      this.historyIndex -= 1;
      this.inputString = this.bashHistory[this.historyIndex];
      $('#input').html(this.inputString);
    } else if (event.which === 40 && this.historyIndex < this.bashHistory.length) { // down
      event.preventDefault();
      this.historyIndex += 1;
      if (this.historyIndex === this.bashHistory.length) this.inputString = '';
      else this.inputString = this.bashHistory[this.historyIndex];
      $('#input').html(this.inputString);
    } else {
      const k = getChar(event);
      this.inputString += k;
      const kSpaceAdjusted = k === ' ' ? ' ' : k;
      $('#input').append(kSpaceAdjusted);
    }
  }

  handleEnter() {
    if (!this.inputString.match(/[^ ]/g)) { // regex for anything but space
      print(this.getPS1String());
    } else {
      print(this.getPS1String() + this.inputString.replace(' ', ' '));
      this.bashHistory.push(this.inputString);
      print(this.executeCommand(this.inputString));
    }
    this.inputString = '';
    $('#input').html('');
    this.historyIndex = this.bashHistory.length;
  }

  executeCommand(inputString) {
    const shellCommand = new ShellCommand(inputString);
    if (!Shell.validCommands().includes(shellCommand.command)) {
      return `${shellCommand.command}: command not found`;
    }
    const evalStr = `this.${shellCommand.command}(shellCommand)`;
    return eval(evalStr).getDefaultOutput();
  }

  findFile(dir, filepath, filetype) {
    if (filepath.length === 0 || filepath === '.') return dir;
    if (filepath[0] === '~') {
      return filepath.length === 1 ? this.fileStructure :
        this.findFile(this.fileStructure, filepath.slice(1), filetype);
    } else if (filepath[0] === '..') {
      return filepath.length === 1 ? dir.parentRef :
        this.findFile(dir.parentRef, filepath.slice(1), filetype);
    }

    let found = null;
    const typeToFind = filepath.length === 1 ? filetype : 'dir';
    dir.children.forEach((child) => {
      if ((filepath[0] === child.name) && (!typeToFind || typeToFind === child.filetype)) {
        found = child;
      }
    });
    return filepath.length === 1 ? found : this.findFile(found, filepath.slice(1), filetype);
  }

  newFile(filepath, filetype) {
    let dir;
    const pathStr = filepath.join('/');
    if (filepath.length === 1) dir = this.currentDir;
    else dir = this.findFile(this.currentDir, filepath.slice(0, -1), 'dir');
    if (!dir) return new ShellCommandResult(null, `${filepath.slice(0, -1).join('/')}: Directory not found`);
    let file;
    if (filetype === 'dir') file = new DirFile(filepath.slice(-1)[0], pathStr, filetype, dir);
    else file = new TxtFile(filepath.slice(-1)[0], pathStr, filetype, dir);
    dir.children.push(file);
    return new ShellCommandResult(null, null, file);
  }

  /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
   *                   shell commands                        *
   * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

  /* return list of valid commands that can be executed */
  static validCommands() {
    return [
      'clear',
      'pwd',
      'whoami',
      'cd',
      'ls',
      'cat',
      'touch',
      'mkdir',
      'echo',
    ];
  }

  clear() {
    $('#terminal-output').html('');
    return new ShellCommandResult();
  }

  pwd() {
    return new ShellCommandResult(this.currentDir.fullPath);
  }

  whoami() {
    return new ShellCommandResult(this.user);
  }

  cd(shellCommand) {
    if (shellCommand.args.length === 0) {
      this.currentDir = this.fileStructure;
      $('#PS1').html(this.getPS1String());
      return new ShellCommandResult();
    }
    const dir = this.findFile(this.currentDir, shellCommand.args[0].split('/'), 'dir');
    if (dir) {
      this.currentDir = dir;
      $('#PS1').html(this.getPS1String());
      return new ShellCommandResult();
    }
    return new ShellCommandResult(null, `${shellCommand.args[0]}: directory not found`);
  }

  ls(shellCommand) {
    const res = new ShellCommandResult([]);
    if (shellCommand.args.length === 0) {
      res.stdOut.push(Shell.lsHelper(this.currentDir));
    } else {
      shellCommand.args.forEach((arg) => {
        const dir = this.findFile(this.currentDir, arg.split('/'), 'dir');
        if (!dir) {
          res.stdErr.push(`ls: cannot access ${arg}: no such file or directory`);
        } else {
          let str = '';
          if (shellCommand.args.length > 1) str += `${arg}:`;
          str += Shell.lsHelper(dir);
          res.stdOut.push(str);
        }
      });
    }
    return res;
  }

  static lsHelper(dir) {
    let res = '
';
    dir.children.forEach((child) => {
      res += `<li class="ls ${child.filetype}">${child.name}</li>`;
    });
    res += '
';
    return res;
  }

  cat(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path);
      if (file && file.filetype === 'dir') {
        res.stdErr.push(`cat: ${file.name}: Is a directory`);
      } else if (file) {
        res.stdOut = res.stdOut.concat(file.contents);
      } else {
        res.stdErr.push(`cat: ${arg}: No such file or directory`);
      }
    });
    return res;
  }

  touch(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path, 'txt');
      if (file) file.lastModified = new Date();
      else {
        const newFileRes = this.newFile(path, 'txt');
        res.combine(newFileRes);
      }
    });
    return res;
  }

  mkdir(shellCommand) {
    const res = new ShellCommandResult();
    if (shellCommand.args.length === 0) return res;
    shellCommand.args.forEach((arg) => {
      const path = arg.split('/');
      const file = this.findFile(this.currentDir, path, 'dir');
      if (!file) {
        const newFileRes = this.newFile(path, 'dir');
        res.combine(newFileRes);
      }
    });
    return res;
  }

  echo(shellCommand) {
    const output = shellCommand.args.join(' ');
    return new ShellCommandResult(output);
  }

}
<|endoftext|>


<commit_before>import { useNavigation } from '@react-navigation/native';
import { useTheme } from '@ui-kitten/components';
import * as Linking from 'expo-linking';
import * as Location from 'expo-location';
import propTypes from 'prop-types';
import React from 'react';
import { Alert, AppState, Platform, StyleSheet, useWindowDimensions, View } from 'react-native';
import { Marker, Polyline } from 'react-native-maps';
import { useImmerReducer } from 'use-immer';
import useAuth from '../auth/context';
import Map from '../components/Map';
import MapButton from '../components/MapButton';
import RootView from '../components/RootView';
import VehicleTracking, { initialVehicleTrackingState, vehicleTrackingReducer } from '../components/VehicleTracking';
import config from '../services/config';
import { getIcon } from '../services/icons';
import { getLocation, locationToRegion, useFollowUser } from '../services/location';
import { wrapAsyncWithDelay } from '../services/utilities';
import Report, { REPORT_TYPES } from './Report';

const initialReportState = {
  reportType: REPORT_TYPES.report,
  showReport: false,
};

const reportReducer = (draft, action) => {
  switch (action.type) {
    case 'show':
      draft.showReport = true;
      draft.reportType = action.meta;

      break;
    case 'hide':
      draft.showReport = false;

      break;
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

export default function MainScreen() {
  const navigation = useNavigation();
  const [initialLocation, setInitialLocation] = React.useState(null);
  const appState = React.useRef(AppState.currentState);
  const [showSpinner, setShowSpinner] = React.useState(false);
  const mapView = React.useRef(null);
  const { authInfo } = useAuth();
  // eslint-disable-next-line no-unused-vars
  const [hasUnsubmittedReports, setHasUnsubmittedReports] = React.useState(false);
  const theme = useTheme();
  const mapDimensions = React.useRef(null);
  const [reportHeight, setReportHeight] = React.useState(0);
  const [carcassCoordinates, setCarcassCoordinates] = React.useState(null);
  const [reportState, dispatchReportState] = useImmerReducer(reportReducer, initialReportState);
  const { followUser, stopFollowUser, isFollowing } = useFollowUser(mapView);
  const [vehicleTrackingState, vehicleTrackingDispatch] = useImmerReducer(
    vehicleTrackingReducer,
    initialVehicleTrackingState
  );

  React.useEffect(() => {
    const initLocation = async () => {
      const result = await Location.requestForegroundPermissionsAsync();
      if (result.status !== 'granted') {
        Alert.alert('Error', 'Location permission are required to submit reports.', [
          { text: 'OK', onPress: () => Linking.openSettings() },
        ]);

        return;
      }

      const enabled = await Location.hasServicesEnabledAsync();
      if (!enabled) {
        Alert.alert('Error', 'Location services are required to submit reports.', [
          { text: 'OK', onPress: () => Linking.openSettings() },
        ]);
      }

      const location = await getLocation();

      setInitialLocation(location);
    };

    const handleAppStateChange = async (nextAppState) => {
      if (appState.current.match(/inactive|background/) && nextAppState === 'active' && !initialLocation) {
        await wrapAsyncWithDelay(
          initLocation,
          () => setShowSpinner(true),
          () => setShowSpinner(false),
          config.SPINNER_DELAY
        );
      }

      appState.current = nextAppState;
    };

    AppState.addEventListener('change', handleAppStateChange);

    wrapAsyncWithDelay(
      initLocation,
      () => setShowSpinner(true),
      () => setShowSpinner(false),
      config.SPINNER_DELAY
    );

    return () => AppState.removeEventListener('change', handleAppStateChange);
  }, []);

  const setMarker = async () => {
    const points = { ...crosshairCoordinates };

    if (Platform.OS === 'android') {
      points.y = points.y * ZOOM_FACTOR;
      points.x = points.x * ZOOM_FACTOR;
    }

    const coordinates = await mapView.current.coordinateForPoint(points);

    setCarcassCoordinates(coordinates);
  };

  const showAddReport = () => {
    const displayReport = (newReportType) => {
      dispatchReportState({
        type: 'show',
        meta: newReportType,
      });

      const zoom = async () => {
        await followUser(config.MAX_ZOOM_LEVEL);
      };

      if (Platform.OS === 'android') {
        // give map padding time to catch up
        setTimeout(zoom, 350);
      } else {
        zoom();
      }
    };

    if (authInfo.user.role === config.USER_TYPES.public) {
      displayReport(REPORT_TYPES.report);
    } else if (authInfo.user.role === config.USER_TYPES.agency || authInfo.user.role === config.USER_TYPES.admin) {
      Alert.alert('I would like to...', null, [
        {
          text: 'Report and pick up a carcass',
          onPress: () => displayReport(REPORT_TYPES.pickup),
        },
        {
          text: 'Report a carcass',
          onPress: () => displayReport(REPORT_TYPES.report),
        },
      ]);
    } else {
      displayReport(REPORT_TYPES.pickup);
    }
  };

  const onRegionChange = (_, event) => {
    if (isFollowing && event.isGesture) {
      stopFollowUser();
    }
  };

  const hideAddReport = () => {
    dispatchReportState({
      type: 'hide',
    });
    setCarcassCoordinates(null);
  };

  const { height, width } = useWindowDimensions();
  const ZOOM_FACTOR = 1.3;
  const mapSizeStyle = {
    ...Platform.select({
      ios: {
        height,
        width,
      },
      android: {
        // the cache tiles look super-pixelated on android, this is a hack to help them look better
        height: height * ZOOM_FACTOR,
        width: width * ZOOM_FACTOR,
        marginTop: -(height * ZOOM_FACTOR - height) / 2,
        marginLeft: -(width * ZOOM_FACTOR - width) / 2,
        transform: [{ scale: 1 / ZOOM_FACTOR }],
      },
    }),
  };

  const CrosshairIcon = getIcon({
    pack: 'font-awesome-5',
    name: 'crosshairs',
    size: CROSSHAIR_SIZE,
    color: theme['color-basic-900'],
  });

  const crosshairCoordinates = mapDimensions.current
    ? Platform.select({
        ios: {
          y: (mapDimensions.current.height - reportHeight) / 2,
          x: mapDimensions.current.width / 2,
        },
        android: {
          y: (mapDimensions.current.height - reportHeight) / 2 / ZOOM_FACTOR,
          x: mapDimensions.current.width / 2 / ZOOM_FACTOR,
        },
      })
    : null;

  return (
    <RootView showSpinner={showSpinner} spinnerMessage="getting current location" style={styles.root}>
      {initialLocation ? (
        <>
          <Map
            initialRegion={locationToRegion(initialLocation)}
            onRegionChange={onRegionChange}
            mapPadding={
              reportState.showReport
                ? Platform.select({
                    ios: {
                      bottom: reportHeight + 15,
                    },
                    android: {
                      bottom: reportHeight,
                    },
                  })
                : null
            }
            onLayout={(event) => {
              const { width, height } = event.nativeEvent.layout;
              mapDimensions.current = { width, height };
            }}
            innerRef={mapView}
            style={[styles.map, mapSizeStyle]}
            showsUserLocation={true}
          >
            {vehicleTrackingState.routeCoordinates.length ? (
              <Polyline
                coordinates={vehicleTrackingState.routeCoordinates}
                strokeColor="black"
                strokeWidth={8}
                zIndex={1}
              />
            ) : null}
            {carcassCoordinates ? <Marker coordinate={carcassCoordinates} zIndex={2} /> : null}
          </Map>
          {reportState.showReport && reportHeight > 0 ? (
            <View
              pointerEvents="none"
              style={[styles.crosshairContainer, { top: crosshairCoordinates.y, left: crosshairCoordinates.x }]}
            >
              <CrosshairIcon />
            </View>
          ) : null}
        </>
      ) : null}
      <View style={styles.controlContainer}>
        <View>
          <MapButton
            iconPack="material-community"
            iconName="menu"
            onPress={navigation.openDrawer}
            showAlert={hasUnsubmittedReports}
          />
        </View>
        <View>
          <MapButton
            iconPack="font-awesome"
            iconName={isFollowing ? 'location-arrow' : 'location-arrow'}
            onPress={() => followUser()}
            color={isFollowing ? theme['color-info-600'] : null}
          />
        </View>
      </View>
      <View style={styles.controlContainer}>
        <View></View>
        <View style={styles.bottomContainer}>
          {authInfo.user.role === config.USER_TYPES.public ? null : (
            <MapButton
              iconPack="material"
              iconName="drive-eta"
              onPress={() => vehicleTrackingDispatch({ type: 'SHOW' })}
              style={styles.topButton}
              color={vehicleTrackingState.isTracking ? theme['color-info-600'] : null}
            />
          )}
          <MapButton iconPack="material" iconName="add-circle" onPress={showAddReport} />
        </View>
      </View>
      <Report
        show={reportState.showReport}
        reportType={reportState.reportType}
        hideReport={hideAddReport}
        setHeight={setReportHeight}
        setMarker={setMarker}
        carcassCoordinates={carcassCoordinates}
      />
      <VehicleTracking state={vehicleTrackingState} dispatch={vehicleTrackingDispatch} />
    </RootView>
  );
}

MainScreen.propTypes = {
  navigation: propTypes.object,
};

const MAP_PADDING = 20;
const CROSSHAIR_SIZE = 40;
const styles = StyleSheet.create({
  root: {
    justifyContent: 'space-between',
  },
  map: {
    position: 'absolute',
    top: 0,
  },
  controlContainer: {
    paddingHorizontal: MAP_PADDING,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  bottomContainer: {
    paddingBottom: MAP_PADDING,
  },
  topButton: {
    marginBottom: MAP_PADDING / 2,
  },
  crosshairContainer: {
    position: 'absolute',
    marginLeft: -CROSSHAIR_SIZE / 2,
    marginTop: -CROSSHAIR_SIZE / 2,
  },
});
<commit_msg>fix: fix bug causing issues with app switching and location<commit_after>import { useNavigation } from '@react-navigation/native';
import { useTheme } from '@ui-kitten/components';
import * as Linking from 'expo-linking';
import * as Location from 'expo-location';
import propTypes from 'prop-types';
import React from 'react';
import { Alert, AppState, Platform, StyleSheet, useWindowDimensions, View } from 'react-native';
import { Marker, Polyline } from 'react-native-maps';
import { useImmerReducer } from 'use-immer';
import useAuth from '../auth/context';
import Map from '../components/Map';
import MapButton from '../components/MapButton';
import RootView from '../components/RootView';
import VehicleTracking, { initialVehicleTrackingState, vehicleTrackingReducer } from '../components/VehicleTracking';
import config from '../services/config';
import { getIcon } from '../services/icons';
import { getLocation, locationToRegion, useFollowUser } from '../services/location';
import { wrapAsyncWithDelay } from '../services/utilities';
import Report, { REPORT_TYPES } from './Report';

const initialReportState = {
  reportType: REPORT_TYPES.report,
  showReport: false,
};

const reportReducer = (draft, action) => {
  switch (action.type) {
    case 'show':
      draft.showReport = true;
      draft.reportType = action.meta;

      break;
    case 'hide':
      draft.showReport = false;

      break;
    default:
      throw new Error(`Unknown action type: ${action.type}`);
  }
};

export default function MainScreen() {
  const navigation = useNavigation();
  const [initialLocation, setInitialLocation] = React.useState(null);
  const appState = React.useRef(AppState.currentState);
  const [showSpinner, setShowSpinner] = React.useState(false);
  const mapView = React.useRef(null);
  const { authInfo } = useAuth();
  // eslint-disable-next-line no-unused-vars
  const [hasUnsubmittedReports, setHasUnsubmittedReports] = React.useState(false);
  const theme = useTheme();
  const mapDimensions = React.useRef(null);
  const [reportHeight, setReportHeight] = React.useState(0);
  const [carcassCoordinates, setCarcassCoordinates] = React.useState(null);
  const [reportState, dispatchReportState] = useImmerReducer(reportReducer, initialReportState);
  const { followUser, stopFollowUser, isFollowing } = useFollowUser(mapView);
  const [vehicleTrackingState, vehicleTrackingDispatch] = useImmerReducer(
    vehicleTrackingReducer,
    initialVehicleTrackingState
  );

  const initLocation = async () => {
    const result = await Location.requestForegroundPermissionsAsync();
    if (!result.granted) {
      Alert.alert('Error', 'Location permissions are required to submit reports.', [
        { text: 'OK', onPress: () => Linking.openSettings() },
      ]);

      return;
    }

    const enabled = await Location.hasServicesEnabledAsync();
    if (!enabled) {
      Alert.alert('Error', 'Location services are required to submit reports.', [
        { text: 'OK', onPress: () => Linking.openSettings() },
      ]);
    }

    const location = await getLocation();

    setInitialLocation(location);
  };

  const handleAppStateChange = async (nextAppState) => {
    if (appState.current.match(/inactive|background/) && nextAppState === 'active' && !initialLocation) {
      await wrapAsyncWithDelay(
        initLocation,
        () => setShowSpinner(true),
        () => setShowSpinner(false),
        config.SPINNER_DELAY
      );
    }

    appState.current = nextAppState;
  };

  React.useEffect(() => {
    const handle = AppState.addEventListener('change', handleAppStateChange);

    wrapAsyncWithDelay(
      initLocation,
      () => setShowSpinner(true),
      () => setShowSpinner(false),
      config.SPINNER_DELAY
    );

    return () => handle?.remove();
  }, []);

  const setMarker = async () => {
    const points = { ...crosshairCoordinates };

    if (Platform.OS === 'android') {
      points.y = points.y * ZOOM_FACTOR;
      points.x = points.x * ZOOM_FACTOR;
    }

    const coordinates = await mapView.current.coordinateForPoint(points);

    setCarcassCoordinates(coordinates);
  };

  const showAddReport = () => {
    const displayReport = (newReportType) => {
      dispatchReportState({
        type: 'show',
        meta: newReportType,
      });

      const zoom = async () => {
        await followUser(config.MAX_ZOOM_LEVEL);
      };

      if (Platform.OS === 'android') {
        // give map padding time to catch up
        setTimeout(zoom, 350);
      } else {
        zoom();
      }
    };

    if (authInfo.user.role === config.USER_TYPES.public) {
      displayReport(REPORT_TYPES.report);
    } else if (authInfo.user.role === config.USER_TYPES.agency || authInfo.user.role === config.USER_TYPES.admin) {
      Alert.alert('I would like to...', null, [
        {
          text: 'Report and pick up a carcass',
          onPress: () => displayReport(REPORT_TYPES.pickup),
        },
        {
          text: 'Report a carcass',
          onPress: () => displayReport(REPORT_TYPES.report),
        },
      ]);
    } else {
      displayReport(REPORT_TYPES.pickup);
    }
  };

  const onRegionChange = (_, event) => {
    if (isFollowing && event.isGesture) {
      stopFollowUser();
    }
  };

  const hideAddReport = () => {
    dispatchReportState({
      type: 'hide',
    });
    setCarcassCoordinates(null);
  };

  const { height, width } = useWindowDimensions();
  const ZOOM_FACTOR = 1.3;
  const mapSizeStyle = {
    ...Platform.select({
      ios: {
        height,
        width,
      },
      android: {
        // the cache tiles look super-pixelated on android, this is a hack to help them look better
        height: height * ZOOM_FACTOR,
        width: width * ZOOM_FACTOR,
        marginTop: -(height * ZOOM_FACTOR - height) / 2,
        marginLeft: -(width * ZOOM_FACTOR - width) / 2,
        transform: [{ scale: 1 / ZOOM_FACTOR }],
      },
    }),
  };

  const CrosshairIcon = getIcon({
    pack: 'font-awesome-5',
    name: 'crosshairs',
    size: CROSSHAIR_SIZE,
    color: theme['color-basic-900'],
  });

  const crosshairCoordinates = mapDimensions.current
    ? Platform.select({
        ios: {
          y: (mapDimensions.current.height - reportHeight) / 2,
          x: mapDimensions.current.width / 2,
        },
        android: {
          y: (mapDimensions.current.height - reportHeight) / 2 / ZOOM_FACTOR,
          x: mapDimensions.current.width / 2 / ZOOM_FACTOR,
        },
      })
    : null;

  return (
    <RootView showSpinner={showSpinner} spinnerMessage="getting current location" style={styles.root}>
      {initialLocation ? (
        <>
          <Map
            initialRegion={locationToRegion(initialLocation)}
            onRegionChange={onRegionChange}
            mapPadding={
              reportState.showReport
                ? Platform.select({
                    ios: {
                      bottom: reportHeight + 15,
                    },
                    android: {
                      bottom: reportHeight,
                    },
                  })
                : null
            }
            onLayout={(event) => {
              const { width, height } = event.nativeEvent.layout;
              mapDimensions.current = { width, height };
            }}
            innerRef={mapView}
            style={[styles.map, mapSizeStyle]}
            showsUserLocation={true}
          >
            {vehicleTrackingState.routeCoordinates.length ? (
              <Polyline
                coordinates={vehicleTrackingState.routeCoordinates}
                strokeColor="black"
                strokeWidth={8}
                zIndex={1}
              />
            ) : null}
            {carcassCoordinates ? <Marker coordinate={carcassCoordinates} zIndex={2} /> : null}
          </Map>
          {reportState.showReport && reportHeight > 0 ? (
            <View
              pointerEvents="none"
              style={[styles.crosshairContainer, { top: crosshairCoordinates.y, left: crosshairCoordinates.x }]}
            >
              <CrosshairIcon />
            </View>
          ) : null}
        </>
      ) : null}
      <View style={styles.controlContainer}>
        <View>
          <MapButton
            iconPack="material-community"
            iconName="menu"
            onPress={navigation.openDrawer}
            showAlert={hasUnsubmittedReports}
          />
        </View>
        <View>
          <MapButton
            iconPack="font-awesome"
            iconName={isFollowing ? 'location-arrow' : 'location-arrow'}
            onPress={() => followUser()}
            color={isFollowing ? theme['color-info-600'] : null}
          />
        </View>
      </View>
      <View style={styles.controlContainer}>
        <View></View>
        <View style={styles.bottomContainer}>
          {authInfo.user.role === config.USER_TYPES.public ? null : (
            <MapButton
              iconPack="material"
              iconName="drive-eta"
              onPress={() => vehicleTrackingDispatch({ type: 'SHOW' })}
              style={styles.topButton}
              color={vehicleTrackingState.isTracking ? theme['color-info-600'] : null}
            />
          )}
          <MapButton iconPack="material" iconName="add-circle" onPress={showAddReport} />
        </View>
      </View>
      <Report
        show={reportState.showReport}
        reportType={reportState.reportType}
        hideReport={hideAddReport}
        setHeight={setReportHeight}
        setMarker={setMarker}
        carcassCoordinates={carcassCoordinates}
      />
      <VehicleTracking state={vehicleTrackingState} dispatch={vehicleTrackingDispatch} />
    </RootView>
  );
}

MainScreen.propTypes = {
  navigation: propTypes.object,
};

const MAP_PADDING = 20;
const CROSSHAIR_SIZE = 40;
const styles = StyleSheet.create({
  root: {
    justifyContent: 'space-between',
  },
  map: {
    position: 'absolute',
    top: 0,
  },
  controlContainer: {
    paddingHorizontal: MAP_PADDING,
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  bottomContainer: {
    paddingBottom: MAP_PADDING,
  },
  topButton: {
    marginBottom: MAP_PADDING / 2,
  },
  crosshairContainer: {
    position: 'absolute',
    marginLeft: -CROSSHAIR_SIZE / 2,
    marginTop: -CROSSHAIR_SIZE / 2,
  },
});
<|endoftext|>


<commit_before>/* ========================================================================
 * Bootstrap: carousel.js v3.1.1
 * http://getbootstrap.com/javascript/#carousel
 * ========================================================================
 * Copyright 2011-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * ======================================================================== */

+function ($) {
  'use strict';

  // CAROUSEL CLASS DEFINITION
  // =========================

  var Carousel = function (element, options) {
    this.$element    = $(element)
    this.$indicators = this.$element.find('.carousel-indicators')
    this.options     = options
    this.paused      =
    this.sliding     =
    this.interval    =
    this.$active     =
    this.$items      = null

    this.options.pause == 'hover' && this.$element
      .on('mouseenter', $.proxy(this.pause, this))
      .on('mouseleave', $.proxy(this.cycle, this))
  }

  Carousel.DEFAULTS = {
    interval: 5000,
    pause: 'hover',
    wrap: true
  }

  Carousel.prototype.cycle =  function (e) {
    e || (this.paused = false)

    this.interval && clearInterval(this.interval)

    this.options.interval
      && !this.paused
      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))

    return this
  }

  Carousel.prototype.getActiveIndex = function () {
    this.$active = this.$element.find('.item.active')
    this.$items  = this.$active.parent().children('.item')

    return this.$items.index(this.$active)
  }

  Carousel.prototype.to = function (pos) {
    var that        = this
    var activeIndex = this.getActiveIndex()

    if (pos > (this.$items.length - 1) || pos < 0) return

    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid". not a typo. past tense of "to slide".
    if (activeIndex == pos) return this.pause().cycle()

    return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
  }

  Carousel.prototype.pause = function (e) {
    e || (this.paused = true)

    if (this.$element.find('.next, .prev').length && $.support.transition) {
      this.$element.trigger($.support.transition.end)
      this.cycle(true)
    }

    this.interval = clearInterval(this.interval)

    return this
  }

  Carousel.prototype.next = function () {
    if (this.sliding) return
    return this.slide('next')
  }

  Carousel.prototype.prev = function () {
    if (this.sliding) return
    return this.slide('prev')
  }

  Carousel.prototype.slide = function (type, next) {
    var $active   = this.$element.find('.item.active')
    var $next     = next || $active[type]()
    var isCycling = this.interval
    var direction = type == 'next' ? 'left' : 'right'
    var fallback  = type == 'next' ? 'first' : 'last'
    var that      = this

    if (!$next.length) {
      if (!this.options.wrap) return
      $next = this.$element.find('.item')[fallback]()
    }

    if ($next.hasClass('active')) return this.sliding = false

    var relatedTarget = $next[0]
    var e = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction })
    this.$element.trigger(e)
    if (e.isDefaultPrevented()) return

    this.sliding = true

    isCycling && this.pause()

    if (this.$indicators.length) {
      this.$indicators.find('.active').removeClass('active')
      this.$element.one('slid.bs.carousel', function () { // yes, "slid". not a typo. past tense of "to slide".
        var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
        $nextIndicator && $nextIndicator.addClass('active')
      })
    }

    var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid". not a typo. past tense of "to slide".
    if ($.support.transition && this.$element.hasClass('slide')) {
      $next.addClass(type)
      $next[0].offsetWidth // force reflow
      $active.addClass(direction)
      $next.addClass(direction)
      $active
        .one($.support.transition.end, function () {
          $next.removeClass([type, direction].join(' ')).addClass('active')
          $active.removeClass(['active', direction].join(' '))
          that.sliding = false
          setTimeout(function () { that.$element.trigger(slidEvent) }, 0)
        })
        .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
    } else {
      $active.removeClass('active')
      $next.addClass('active')
      this.sliding = false
      this.$element.trigger(slidEvent)
    }

    isCycling && this.cycle()

    return this
  }

  // CAROUSEL PLUGIN DEFINITION
  // ==========================

  var old = $.fn.carousel

  $.fn.carousel = function (option) {
    return this.each(function () {
      var $this   = $(this)
      var data    = $this.data('bs.carousel')
      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
      var action  = typeof option == 'string' ? option : options.slide

      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
      if (typeof option == 'number') data.to(option)
      else if (action) data[action]()
      else if (options.interval) data.pause().cycle()
    })
  }

  $.fn.carousel.Constructor = Carousel

  // CAROUSEL NO CONFLICT
  // ====================

  $.fn.carousel.noConflict = function () {
    $.fn.carousel = old
    return this
  }

  // CAROUSEL DATA-API
  // =================

  $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
    var $this   = $(this), href
    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
    var options = $.extend({}, $target.data(), $this.data())
    var slideIndex = $this.attr('data-slide-to')
    if (slideIndex) options.interval = false

    $target.carousel(options)

    if (slideIndex = $this.attr('data-slide-to')) {
      $target.data('bs.carousel').to(slideIndex)
    }

    e.preventDefault()
  })

  $(window).on('load', function () {
    $('[data-ride="carousel"]').each(function () {
      var $carousel = $(this)
      $carousel.carousel($carousel.data())
    })
  })

}(jQuery);
<commit_msg>change 'slid' comments per @fat's feedback<commit_after>/* ========================================================================
 * Bootstrap: carousel.js v3.1.1
 * http://getbootstrap.com/javascript/#carousel
 * ========================================================================
 * Copyright 2011-2014 Twitter, Inc.
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * ======================================================================== */

+function ($) {
  'use strict';

  // CAROUSEL CLASS DEFINITION
  // =========================

  var Carousel = function (element, options) {
    this.$element    = $(element)
    this.$indicators = this.$element.find('.carousel-indicators')
    this.options     = options
    this.paused      =
    this.sliding     =
    this.interval    =
    this.$active     =
    this.$items      = null

    this.options.pause == 'hover' && this.$element
      .on('mouseenter', $.proxy(this.pause, this))
      .on('mouseleave', $.proxy(this.cycle, this))
  }

  Carousel.DEFAULTS = {
    interval: 5000,
    pause: 'hover',
    wrap: true
  }

  Carousel.prototype.cycle =  function (e) {
    e || (this.paused = false)

    this.interval && clearInterval(this.interval)

    this.options.interval
      && !this.paused
      && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))

    return this
  }

  Carousel.prototype.getActiveIndex = function () {
    this.$active = this.$element.find('.item.active')
    this.$items  = this.$active.parent().children('.item')

    return this.$items.index(this.$active)
  }

  Carousel.prototype.to = function (pos) {
    var that        = this
    var activeIndex = this.getActiveIndex()

    if (pos > (this.$items.length - 1) || pos < 0) return

    if (this.sliding)       return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid"
    if (activeIndex == pos) return this.pause().cycle()

    return this.slide(pos > activeIndex ? 'next' : 'prev', $(this.$items[pos]))
  }

  Carousel.prototype.pause = function (e) {
    e || (this.paused = true)

    if (this.$element.find('.next, .prev').length && $.support.transition) {
      this.$element.trigger($.support.transition.end)
      this.cycle(true)
    }

    this.interval = clearInterval(this.interval)

    return this
  }

  Carousel.prototype.next = function () {
    if (this.sliding) return
    return this.slide('next')
  }

  Carousel.prototype.prev = function () {
    if (this.sliding) return
    return this.slide('prev')
  }

  Carousel.prototype.slide = function (type, next) {
    var $active   = this.$element.find('.item.active')
    var $next     = next || $active[type]()
    var isCycling = this.interval
    var direction = type == 'next' ? 'left' : 'right'
    var fallback  = type == 'next' ? 'first' : 'last'
    var that      = this

    if (!$next.length) {
      if (!this.options.wrap) return
      $next = this.$element.find('.item')[fallback]()
    }

    if ($next.hasClass('active')) return this.sliding = false

    var relatedTarget = $next[0]
    var e = $.Event('slide.bs.carousel', { relatedTarget: relatedTarget, direction: direction })
    this.$element.trigger(e)
    if (e.isDefaultPrevented()) return

    this.sliding = true

    isCycling && this.pause()

    if (this.$indicators.length) {
      this.$indicators.find('.active').removeClass('active')
      this.$element.one('slid.bs.carousel', function () { // yes, "slid"
        var $nextIndicator = $(that.$indicators.children()[that.getActiveIndex()])
        $nextIndicator && $nextIndicator.addClass('active')
      })
    }

    var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid"
    if ($.support.transition && this.$element.hasClass('slide')) {
      $next.addClass(type)
      $next[0].offsetWidth // force reflow
      $active.addClass(direction)
      $next.addClass(direction)
      $active
        .one($.support.transition.end, function () {
          $next.removeClass([type, direction].join(' ')).addClass('active')
          $active.removeClass(['active', direction].join(' '))
          that.sliding = false
          setTimeout(function () { that.$element.trigger(slidEvent) }, 0)
        })
        .emulateTransitionEnd($active.css('transition-duration').slice(0, -1) * 1000)
    } else {
      $active.removeClass('active')
      $next.addClass('active')
      this.sliding = false
      this.$element.trigger(slidEvent)
    }

    isCycling && this.cycle()

    return this
  }

  // CAROUSEL PLUGIN DEFINITION
  // ==========================

  var old = $.fn.carousel

  $.fn.carousel = function (option) {
    return this.each(function () {
      var $this   = $(this)
      var data    = $this.data('bs.carousel')
      var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option)
      var action  = typeof option == 'string' ? option : options.slide

      if (!data) $this.data('bs.carousel', (data = new Carousel(this, options)))
      if (typeof option == 'number') data.to(option)
      else if (action) data[action]()
      else if (options.interval) data.pause().cycle()
    })
  }

  $.fn.carousel.Constructor = Carousel

  // CAROUSEL NO CONFLICT
  // ====================

  $.fn.carousel.noConflict = function () {
    $.fn.carousel = old
    return this
  }

  // CAROUSEL DATA-API
  // =================

  $(document).on('click.bs.carousel.data-api', '[data-slide], [data-slide-to]', function (e) {
    var $this   = $(this), href
    var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
    var options = $.extend({}, $target.data(), $this.data())
    var slideIndex = $this.attr('data-slide-to')
    if (slideIndex) options.interval = false

    $target.carousel(options)

    if (slideIndex = $this.attr('data-slide-to')) {
      $target.data('bs.carousel').to(slideIndex)
    }

    e.preventDefault()
  })

  $(window).on('load', function () {
    $('[data-ride="carousel"]').each(function () {
      var $carousel = $(this)
      $carousel.carousel($carousel.data())
    })
  })

}(jQuery);
<|endoftext|>


<commit_before>var Crafty = require('./core.js'),
    document = window.document;

Crafty.extend({
    /**@
     * #Crafty.audio
     * @category Audio
     *
     * Add sound files and play them. Chooses best format for browser support.
     * Due to the nature of HTML5 audio, three types of audio files will be
     * required for cross-browser capabilities. These formats are MP3, Ogg and WAV.
     * When sound was not muted on before pause, sound will be unmuted after unpause.
     * When sound is muted Crafty.pause() does not have any effect on sound
     *
     * The maximum number of sounds that can be played simultaneously is defined by Crafty.audio.maxChannels.  The default value is 7.
     */
    audio: {

        sounds: {},
        supported: null,
        codecs: { // Chart from jPlayer
            ogg: 'audio/ogg; codecs="vorbis"', //OGG
            wav: 'audio/wav; codecs="1"', // PCM
            webma: 'audio/webm; codecs="vorbis"', // WEBM
            mp3: 'audio/mpeg; codecs="mp3"', //MP3
            m4a: 'audio/mp4; codecs="mp4a.40.2"' // AAC / MP4
        },
        volume: 1, //Global Volume
        muted: false,
        paused: false,
        playCheck: null,
        /**
         * Function to setup supported formats
         **/
        _canPlay: function () {
            this.supported = {};
            // Without support, no formats are supported
            if (!Crafty.support.audio)
                return;
            var audio = this.audioElement(),
                canplay;
            for (var i in this.codecs) {
                canplay = audio.canPlayType(this.codecs[i]);
                if (canplay !== "" && canplay !== "no") {
                    this.supported[i] = true;
                } else {
                    this.supported[i] = false;
                }
            }

        },

        /**@
         * #Crafty.audio.supports
         * @comp Crafty.audio
         * @sign public this Crafty.audio.supports(String extension)
         * @param extension - A file extension to check audio support for
         *
         * Return true if the browser thinks it can play the given file type, otherwise false
         */
        supports: function (extension) {
            // Build cache of supported formats, if necessary
            if (this.supported === null)
                this._canPlay();

            if (this.supported[extension])
                return true;
            else
                return false;
        },

        /**
         * Function to get an Audio Element
         **/
        audioElement: function () {
            //IE does not support Audio Object
            return typeof Audio !== 'undefined' ? new Audio("") : document.createElement('audio');
        },

        /**@
         * #Crafty.audio.create
         * @comp Crafty.audio
         * @sign public this Crafty.audio.create(String id, String url)
         * @param id - A string to refer to sounds
         * @param url - A string pointing to the sound file
         *
         * Creates an audio asset with the given id and resource.  `Crafty.audio.add` is a more flexible interface that allows cross-browser compatibility.
         *
         * If the sound file extension is not supported, returns false; otherwise, returns the audio asset.
         */
        create: function (id, path) {
            //check extension, return if not supported
            var ext = path.substr(path.lastIndexOf('.') + 1).toLowerCase();
            if (!this.supports(ext))
                return false;

            //initiate the audio element
            var audio = this.audioElement();
            audio.id = id;
            audio.preload = "auto";
            audio.volume = Crafty.audio.volume;
            audio.src = path;

            //create an asset and metadata for the audio element
            Crafty.asset(path, audio);
            this.sounds[id] = {
                obj: audio,
                played: 0,
                volume: Crafty.audio.volume
            };
            return this.sounds[id];

        },

        /**@
         * #Crafty.audio.add
         * @comp Crafty.audio
         * @sign public this Crafty.audio.add(String id, String url)
         * @param id - A string to refer to sounds
         * @param url - A string pointing to the sound file
         * @sign public this Crafty.audio.add(String id, Array urls)
         * @param urls - Array of urls pointing to different format of the same sound, selecting the first that is playable
         * @sign public this Crafty.audio.add(Object map)
         * @param map - key-value pairs where the key is the `id` and the value is either a `url` or `urls`
         *
         * Loads a sound to be played. Due to the nature of HTML5 audio,
         * three types of audio files will be required for cross-browser capabilities.
         * These formats are MP3, Ogg and WAV.
         *
         * Passing an array of URLs will determine which format the browser can play and select it over any other.
         *
         * Accepts an object where the key is the audio name and
         * either a URL or an Array of URLs (to determine which type to use).
         *
         * The ID you use will be how you refer to that sound when using `Crafty.audio.play`.
         *
         * @example
         * ~~~
         * //adding audio from an object
         * Crafty.audio.add({
         * shoot: ["sounds/shoot.wav",
         * "sounds/shoot.mp3",
         * "sounds/shoot.ogg"],
         *
         * coin: "sounds/coin.mp3"
         * });
         *
         * //adding a single sound
         * Crafty.audio.add("walk", [
         * "sounds/walk.mp3",
         * "sounds/walk.ogg",
         * "sounds/walk.wav"
         * ]);
         *
         * //only one format
         * Crafty.audio.add("jump", "sounds/jump.mp3");
         * ~~~
         */
        add: function (id, url) {
            if (!Crafty.support.audio)
                return;

            var src;

            if (arguments.length === 1 && typeof id === "object") {
                for (var i in id) {
                    for (src in id[i]) {
                        if (Crafty.audio.create(i, id[i][src]))
                            break;
                    }
                }
            }
            if (typeof id === "string") {
                if (typeof url === "string") {
                    Crafty.audio.create(id, url);
                }

                if (typeof url === "object") {
                    for (src in url) {
                        if (Crafty.audio.create(id, url[src]))
                            break;
                    }
                }

            }
        },
        /**@
         * #Crafty.audio.play
         * @comp Crafty.audio
         * @sign public this Crafty.audio.play(String id)
         * @sign public this Crafty.audio.play(String id, Number repeatCount)
         * @sign public this Crafty.audio.play(String id, Number repeatCount, Number volume)
         * @param id - A string to refer to sounds
         * @param repeatCount - Repeat count for the file, where -1 stands for repeat forever.
         * @param volume - volume can be a number between 0.0 and 1.0
         * @returns The audio element used to play the sound.  Null if the call failed due to a lack of open channels.
         *
         * Will play a sound previously added by using the ID that was used in `Crafty.audio.add`.
         * Has a default maximum of 5 channels so that the same sound can play simultaneously unless all of the channels are playing.

         * *Note that the implementation of HTML5 Audio is buggy at best.*
         *
         * @example
         * ~~~
         * Crafty.audio.play("walk");
         *
         * //play and repeat forever
         * Crafty.audio.play("backgroundMusic", -1);
         * Crafty.audio.play("explosion",1,0.5); //play sound once with volume of 50%
         * ~~~
         */
        play: function (id, repeat, volume) {
            if (repeat === 0 || !Crafty.support.audio || !this.sounds[id])
                return;
            var s = this.sounds[id];
            var c = this.getOpenChannel();
            if (!c)
                return null;
            c.id = id;
            var a = c.obj;

            c.volume = s.volume = s.obj.volume = volume || Crafty.audio.volume;

            a.volume = s.volume;
            a.src = s.obj.src;

            if (this.muted)
                a.volume = 0;
            a.play();
            s.played++;
            c.onEnd = function () {
                if (s.played < repeat || repeat == -1) {
                    if (this.currentTime)
                        this.currentTime = 0;
                    this.play();
                    s.played++;
                } else {
                    c.active = false;
                    this.pause();
                    this.removeEventListener("ended", c.onEnd, true);
                    this.currentTime = 0;
                    Crafty.trigger("SoundComplete", {
                        id: c.id
                    });
                }

            };
            a.addEventListener("ended", c.onEnd, true);

            return a;
        },

        /**@
         * #Crafty.audio.setChannels
         * @comp Crafty.audio
         * @sign public this Crafty.audio.setChannels(Number n)
         * @param n - The maximum number of channels
         */
        maxChannels: 7,
        setChannels: function (n) {
            this.maxChannels = n;
            if (n > channels.length)
                this.channels.length = n;

        },

        channels: [],
        // Finds an unused audio element, marks it as in use, and return it.
        getOpenChannel: function () {
            for (var i = 0; i < this.channels.length; i++) {
                if (this.channels[i].active === false) {
                    this.channels[i].active = true;
                    return this.channels[i];
                }
            }
            // If necessary, create a new element, unless we've already reached the max limit
            if (i <= this.maxChannels) {
                var c = {
                    obj: this.audioElement(),
                    active: true,
                    // Checks that the channel is being used to play sound id
                    _is: function (id) {
                        return this.id === id && this.active;
                    }
                };
                this.channels.push(c);
                return c;
            }
            // In that case, return null
            return null;
        },

        /**@
         * #Crafty.audio.remove
         * @comp Crafty.audio
         * @sign public this Crafty.audio.remove([String id])
         * @param id - A string to refer to sounds
         *
         * Will stop the sound and remove all references to the audio object allowing the browser to free the memory.
         * If no id is given, all sounds will be removed.
         *
         * @example
         * ~~~
         * Crafty.audio.remove("walk");
         * ~~~
         */
        remove: function (id) {
            if (!Crafty.support.audio)
                return;

            var s;

            if (!id) {
                for (var i in this.sounds) {
                    s = this.sounds[i];
                    Crafty.audio.stop(id);
                    delete Crafty.assets[s.obj.src];
                    delete Crafty.audio.sounds[id];
                }
                return;
            }
            if (!this.sounds[id])
                return;

            s = this.sounds[id];
            Crafty.audio.stop(id);
            delete Crafty.assets[s.obj.src];
            delete Crafty.audio.sounds[id];
        },
        /**@
         * #Crafty.audio.stop
         * @comp Crafty.audio
         * @sign public this Crafty.audio.stop([Number ID])
         *
         * Stops any playing sound. if id is not set, stop all sounds which are playing
         *
         * @example
         * ~~~
         * //all sounds stopped playing now
         * Crafty.audio.stop();
         *
         * ~~~
         */
        stop: function (id) {
            if (!Crafty.support.audio)
                return;
            var s;
            if (!id) {
                for (var i in this.channels) {

                    c = this.channels[i];
                    if (c.active) {
                        c.active = false;
                        c.obj.pause();
                    }
                }
                return;
            }

            s = this.sounds[id];
            if (!s)
                return;
            if (!s.obj.paused)
                s.obj.pause();
        },
        /**
         * #Crafty.audio._mute
         * @comp Crafty.audio
         * @sign public this Crafty.audio._mute([Boolean mute])
         *
         * Mute or unmute every Audio instance that is playing.
         */
        _mute: function (mute) {
            if (!Crafty.support.audio)
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                c.obj.volume = mute ? 0 : c.volume;
            }
            this.muted = mute;
        },
        /**@
         * #Crafty.audio.toggleMute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.toggleMute()
         *
         * Mute or unmute every Audio instance that is playing. Toggles between
         * pausing or playing depending on the state.
         *
         * @example
         * ~~~
         * //toggle mute and unmute depending on current state
         * Crafty.audio.toggleMute();
         * ~~~
         */
        toggleMute: function () {
            if (!this.muted) {
                this._mute(true);
            } else {
                this._mute(false);
            }

        },
        /**@
         * #Crafty.audio.mute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.mute()
         *
         * Mute every Audio instance that is playing.
         *
         * @example
         * ~~~
         * Crafty.audio.mute();
         * ~~~
         */
        mute: function () {
            this._mute(true);
        },
        /**@
         * #Crafty.audio.unmute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.unmute()
         *
         * Unmute every Audio instance that is playing.
         *
         * @example
         * ~~~
         * Crafty.audio.unmute();
         * ~~~
         */
        unmute: function () {
            this._mute(false);
        },

        /**@
         * #Crafty.audio.pause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.pause(string ID)
         *
         * Pause the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.pause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to pause
         */
        pause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id) && !c.obj.paused)
                    c.obj.pause();
            }

        },

        /**@
         * #Crafty.audio.unpause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.unpause(string ID)
         *
         * Resume playing the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.unpause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to unpause
         */
        unpause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id) && c.obj.paused)
                    c.obj.play();
            }
        },

        /**@
         * #Crafty.audio.togglePause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.togglePause(string ID)
         *
         * Toggle the pause status of the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.togglePause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to pause/unpause
         */
        togglePause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id)) {
                    if (c.obj.paused) {
                        c.obj.play();
                    } else {
                        c.obj.pause();
                    }
                }
            }
        }
    }
});<commit_msg>Fix bug when using Crafty.audio.stop on a specific sound<commit_after>var Crafty = require('./core.js'),
    document = window.document;

Crafty.extend({
    /**@
     * #Crafty.audio
     * @category Audio
     *
     * Add sound files and play them. Chooses best format for browser support.
     * Due to the nature of HTML5 audio, three types of audio files will be
     * required for cross-browser capabilities. These formats are MP3, Ogg and WAV.
     * When sound was not muted on before pause, sound will be unmuted after unpause.
     * When sound is muted Crafty.pause() does not have any effect on sound
     *
     * The maximum number of sounds that can be played simultaneously is defined by Crafty.audio.maxChannels.  The default value is 7.
     */
    audio: {

        sounds: {},
        supported: null,
        codecs: { // Chart from jPlayer
            ogg: 'audio/ogg; codecs="vorbis"', //OGG
            wav: 'audio/wav; codecs="1"', // PCM
            webma: 'audio/webm; codecs="vorbis"', // WEBM
            mp3: 'audio/mpeg; codecs="mp3"', //MP3
            m4a: 'audio/mp4; codecs="mp4a.40.2"' // AAC / MP4
        },
        volume: 1, //Global Volume
        muted: false,
        paused: false,
        playCheck: null,
        /**
         * Function to setup supported formats
         **/
        _canPlay: function () {
            this.supported = {};
            // Without support, no formats are supported
            if (!Crafty.support.audio)
                return;
            var audio = this.audioElement(),
                canplay;
            for (var i in this.codecs) {
                canplay = audio.canPlayType(this.codecs[i]);
                if (canplay !== "" && canplay !== "no") {
                    this.supported[i] = true;
                } else {
                    this.supported[i] = false;
                }
            }

        },

        /**@
         * #Crafty.audio.supports
         * @comp Crafty.audio
         * @sign public this Crafty.audio.supports(String extension)
         * @param extension - A file extension to check audio support for
         *
         * Return true if the browser thinks it can play the given file type, otherwise false
         */
        supports: function (extension) {
            // Build cache of supported formats, if necessary
            if (this.supported === null)
                this._canPlay();

            if (this.supported[extension])
                return true;
            else
                return false;
        },

        /**
         * Function to get an Audio Element
         **/
        audioElement: function () {
            //IE does not support Audio Object
            return typeof Audio !== 'undefined' ? new Audio("") : document.createElement('audio');
        },

        /**@
         * #Crafty.audio.create
         * @comp Crafty.audio
         * @sign public this Crafty.audio.create(String id, String url)
         * @param id - A string to refer to sounds
         * @param url - A string pointing to the sound file
         *
         * Creates an audio asset with the given id and resource.  `Crafty.audio.add` is a more flexible interface that allows cross-browser compatibility.
         *
         * If the sound file extension is not supported, returns false; otherwise, returns the audio asset.
         */
        create: function (id, path) {
            //check extension, return if not supported
            var ext = path.substr(path.lastIndexOf('.') + 1).toLowerCase();
            if (!this.supports(ext))
                return false;

            //initiate the audio element
            var audio = this.audioElement();
            audio.id = id;
            audio.preload = "auto";
            audio.volume = Crafty.audio.volume;
            audio.src = path;

            //create an asset and metadata for the audio element
            Crafty.asset(path, audio);
            this.sounds[id] = {
                obj: audio,
                played: 0,
                volume: Crafty.audio.volume
            };
            return this.sounds[id];

        },

        /**@
         * #Crafty.audio.add
         * @comp Crafty.audio
         * @sign public this Crafty.audio.add(String id, String url)
         * @param id - A string to refer to sounds
         * @param url - A string pointing to the sound file
         * @sign public this Crafty.audio.add(String id, Array urls)
         * @param urls - Array of urls pointing to different format of the same sound, selecting the first that is playable
         * @sign public this Crafty.audio.add(Object map)
         * @param map - key-value pairs where the key is the `id` and the value is either a `url` or `urls`
         *
         * Loads a sound to be played. Due to the nature of HTML5 audio,
         * three types of audio files will be required for cross-browser capabilities.
         * These formats are MP3, Ogg and WAV.
         *
         * Passing an array of URLs will determine which format the browser can play and select it over any other.
         *
         * Accepts an object where the key is the audio name and
         * either a URL or an Array of URLs (to determine which type to use).
         *
         * The ID you use will be how you refer to that sound when using `Crafty.audio.play`.
         *
         * @example
         * ~~~
         * //adding audio from an object
         * Crafty.audio.add({
         * shoot: ["sounds/shoot.wav",
         * "sounds/shoot.mp3",
         * "sounds/shoot.ogg"],
         *
         * coin: "sounds/coin.mp3"
         * });
         *
         * //adding a single sound
         * Crafty.audio.add("walk", [
         * "sounds/walk.mp3",
         * "sounds/walk.ogg",
         * "sounds/walk.wav"
         * ]);
         *
         * //only one format
         * Crafty.audio.add("jump", "sounds/jump.mp3");
         * ~~~
         */
        add: function (id, url) {
            if (!Crafty.support.audio)
                return;

            var src;

            if (arguments.length === 1 && typeof id === "object") {
                for (var i in id) {
                    for (src in id[i]) {
                        if (Crafty.audio.create(i, id[i][src]))
                            break;
                    }
                }
            }
            if (typeof id === "string") {
                if (typeof url === "string") {
                    Crafty.audio.create(id, url);
                }

                if (typeof url === "object") {
                    for (src in url) {
                        if (Crafty.audio.create(id, url[src]))
                            break;
                    }
                }

            }
        },
        /**@
         * #Crafty.audio.play
         * @comp Crafty.audio
         * @sign public this Crafty.audio.play(String id)
         * @sign public this Crafty.audio.play(String id, Number repeatCount)
         * @sign public this Crafty.audio.play(String id, Number repeatCount, Number volume)
         * @param id - A string to refer to sounds
         * @param repeatCount - Repeat count for the file, where -1 stands for repeat forever.
         * @param volume - volume can be a number between 0.0 and 1.0
         * @returns The audio element used to play the sound.  Null if the call failed due to a lack of open channels.
         *
         * Will play a sound previously added by using the ID that was used in `Crafty.audio.add`.
         * Has a default maximum of 5 channels so that the same sound can play simultaneously unless all of the channels are playing.

         * *Note that the implementation of HTML5 Audio is buggy at best.*
         *
         * @example
         * ~~~
         * Crafty.audio.play("walk");
         *
         * //play and repeat forever
         * Crafty.audio.play("backgroundMusic", -1);
         * Crafty.audio.play("explosion",1,0.5); //play sound once with volume of 50%
         * ~~~
         */
        play: function (id, repeat, volume) {
            if (repeat === 0 || !Crafty.support.audio || !this.sounds[id])
                return;
            var s = this.sounds[id];
            var c = this.getOpenChannel();
            if (!c)
                return null;
            c.id = id;
            var a = c.obj;

            c.volume = s.volume = s.obj.volume = volume || Crafty.audio.volume;

            a.volume = s.volume;
            a.src = s.obj.src;

            if (this.muted)
                a.volume = 0;
            a.play();
            s.played++;
            c.onEnd = function () {
                if (s.played < repeat || repeat == -1) {
                    if (this.currentTime)
                        this.currentTime = 0;
                    this.play();
                    s.played++;
                } else {
                    c.active = false;
                    this.pause();
                    this.removeEventListener("ended", c.onEnd, true);
                    this.currentTime = 0;
                    Crafty.trigger("SoundComplete", {
                        id: c.id
                    });
                }

            };
            a.addEventListener("ended", c.onEnd, true);

            return a;
        },

        /**@
         * #Crafty.audio.setChannels
         * @comp Crafty.audio
         * @sign public this Crafty.audio.setChannels(Number n)
         * @param n - The maximum number of channels
         */
        maxChannels: 7,
        setChannels: function (n) {
            this.maxChannels = n;
            if (n > channels.length)
                this.channels.length = n;

        },

        channels: [],
        // Finds an unused audio element, marks it as in use, and return it.
        getOpenChannel: function () {
            for (var i = 0; i < this.channels.length; i++) {
                if (this.channels[i].active === false) {
                    this.channels[i].active = true;
                    return this.channels[i];
                }
            }
            // If necessary, create a new element, unless we've already reached the max limit
            if (i <= this.maxChannels) {
                var c = {
                    obj: this.audioElement(),
                    active: true,
                    // Checks that the channel is being used to play sound id
                    _is: function (id) {
                        return this.id === id && this.active;
                    }
                };
                this.channels.push(c);
                return c;
            }
            // In that case, return null
            return null;
        },

        /**@
         * #Crafty.audio.remove
         * @comp Crafty.audio
         * @sign public this Crafty.audio.remove([String id])
         * @param id - A string to refer to sounds
         *
         * Will stop the sound and remove all references to the audio object allowing the browser to free the memory.
         * If no id is given, all sounds will be removed.
         *
         * @example
         * ~~~
         * Crafty.audio.remove("walk");
         * ~~~
         */
        remove: function (id) {
            if (!Crafty.support.audio)
                return;

            var s;

            if (!id) {
                for (var i in this.sounds) {
                    s = this.sounds[i];
                    Crafty.audio.stop(id);
                    delete Crafty.assets[s.obj.src];
                    delete Crafty.audio.sounds[id];
                }
                return;
            }
            if (!this.sounds[id])
                return;

            s = this.sounds[id];
            Crafty.audio.stop(id);
            delete Crafty.assets[s.obj.src];
            delete Crafty.audio.sounds[id];
        },
        /**@
         * #Crafty.audio.stop
         * @comp Crafty.audio
         * @sign public this Crafty.audio.stop([Number ID])
         *
         * Stops any playing sound. if id is not set, stop all sounds which are playing
         *
         * @example
         * ~~~
         * //all sounds stopped playing now
         * Crafty.audio.stop();
         *
         * ~~~
         */
        stop: function (id) {
            if (!Crafty.support.audio)
                return;
            for (var i in this.channels) {
                c = this.channels[i];
                if ( (!id && c.active) || c._is(id) ) {
                    c.active = false;
                    c.obj.pause();
                }
            }
            return;
        },
        /**
         * #Crafty.audio._mute
         * @comp Crafty.audio
         * @sign public this Crafty.audio._mute([Boolean mute])
         *
         * Mute or unmute every Audio instance that is playing.
         */
        _mute: function (mute) {
            if (!Crafty.support.audio)
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                c.obj.volume = mute ? 0 : c.volume;
            }
            this.muted = mute;
        },
        /**@
         * #Crafty.audio.toggleMute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.toggleMute()
         *
         * Mute or unmute every Audio instance that is playing. Toggles between
         * pausing or playing depending on the state.
         *
         * @example
         * ~~~
         * //toggle mute and unmute depending on current state
         * Crafty.audio.toggleMute();
         * ~~~
         */
        toggleMute: function () {
            if (!this.muted) {
                this._mute(true);
            } else {
                this._mute(false);
            }

        },
        /**@
         * #Crafty.audio.mute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.mute()
         *
         * Mute every Audio instance that is playing.
         *
         * @example
         * ~~~
         * Crafty.audio.mute();
         * ~~~
         */
        mute: function () {
            this._mute(true);
        },
        /**@
         * #Crafty.audio.unmute
         * @comp Crafty.audio
         * @sign public this Crafty.audio.unmute()
         *
         * Unmute every Audio instance that is playing.
         *
         * @example
         * ~~~
         * Crafty.audio.unmute();
         * ~~~
         */
        unmute: function () {
            this._mute(false);
        },

        /**@
         * #Crafty.audio.pause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.pause(string ID)
         *
         * Pause the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.pause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to pause
         */
        pause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id) && !c.obj.paused)
                    c.obj.pause();
            }

        },

        /**@
         * #Crafty.audio.unpause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.unpause(string ID)
         *
         * Resume playing the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.unpause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to unpause
         */
        unpause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id) && c.obj.paused)
                    c.obj.play();
            }
        },

        /**@
         * #Crafty.audio.togglePause
         * @comp Crafty.audio
         * @sign public this Crafty.audio.togglePause(string ID)
         *
         * Toggle the pause status of the Audio instance specified by id param.
         *
         * @example
         * ~~~
         * Crafty.audio.togglePause('music');
         * ~~~
         *
         * @param {string} id The id of the audio object to pause/unpause
         */
        togglePause: function (id) {
            if (!Crafty.support.audio || !id || !this.sounds[id])
                return;
            var c;
            for (var i in this.channels) {
                c = this.channels[i];
                if (c._is(id)) {
                    if (c.obj.paused) {
                        c.obj.play();
                    } else {
                        c.obj.pause();
                    }
                }
            }
        }
    }
});<|endoftext|>


<commit_before>// Require dependencies
var _ = require( 'lodash' );
var path = require( 'path' );

function setUpAppPaths( done ) {
	this.appPaths = this.appPaths || {};
	// Set up path maps
	_.assign( this.appPaths, _.defaults( _.clone( this.install['app-paths'] || {} ), {
		'composer-path': '.',
		'vendor-dir': 'vendor',
		'wp-path': 'wp',
		'content-path': 'wp-content',
		'root': '.'
	} ) );
	this.appPaths['theme-path'] = path.join( this.appPaths['content-path'], 'themes' );
	this.appPaths['plugin-path'] = path.join( this.appPaths['content-path'], 'plugins' );
	this.appPaths['mu-plugin-path'] = path.join( this.appPaths['content-path'], 'mu-plugins' );
	done();
}

function getAppPath( name, root, additional ) {
	root = root || '.';
	additional = additional || '.';
	return path.normalize( path.join( root, this.appPaths[ name ], additional ) );
}

module.exports = {
	setUpAppPaths: setUpAppPaths,
	getAppPath: getAppPath
};
<commit_msg>add doc-blocks for the app-paths<commit_after>/**
 * Methods for setting up applications paths for the selected install.
 *
 * This API gives access to all of the customizable locations for the various
 * tools output by Generator VVV. This includes the composer path, vendor
 * directory, WordPress path, wp-content folder path, and site root path.
 *
 * The `getAppPath` method abstracts getting these paths from the base object
 * prepending and appending paths to them as necessary based on context.
 */

// Require dependencies
var _ = require( 'lodash' );
var path = require( 'path' );

/**
 * Sets up the customizable tool paths for the selected site install.
 *
 * In the `wpmanifest.json` file you can specify an `app-paths` object and
 * choose a custom directory for each of the default items (composer, the
 * vendor directory, the core WordPress path, the wp-content folder name and
 * locale, and the site-root location).
 *
 * This methods adds this to the appPaths object and intuits the locations of
 * the plugins, themes, and mu-plugins folders.
 *
 * @param {Function} done Callack to tell the environment work is done.
 */
function setUpAppPaths( done ) {
	this.appPaths = this.appPaths || {};
	// Set up path maps
	_.assign( this.appPaths, _.defaults( _.clone( this.install['app-paths'] || {} ), {
		'composer-path': '.',
		'vendor-dir': 'vendor',
		'wp-path': 'wp',
		'content-path': 'wp-content',
		'root': '.'
	} ) );
	this.appPaths['theme-path'] = path.join( this.appPaths['content-path'], 'themes' );
	this.appPaths['plugin-path'] = path.join( this.appPaths['content-path'], 'plugins' );
	this.appPaths['mu-plugin-path'] = path.join( this.appPaths['content-path'], 'mu-plugins' );
	done();
}

/**
 * Helper function for getting and app-path with optional root and path args.
 *
 * @param  {String} name       The name of the app-path to retrieve.
 * @param  {String} root       The root of the app itself based on context.
 * @param  {String} additional Additional path segments to add to the app-path.
 * @return {String}            The normalized path as requested.
 */
function getAppPath( name, root, additional ) {
	root = root || '.';
	additional = additional || '.';
	return path.normalize( path.join( root, this.appPaths[ name ], additional ) );
}

// Export these methods to mix with the base object prototype.
module.exports = {
	setUpAppPaths: setUpAppPaths,
	getAppPath: getAppPath
};
<|endoftext|>


<commit_before>/*
Copyright (c) 2013, Goldark SS LTDA <http://www.goldark.co>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
-Neither the name of the Goldark nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
*/

/*
This module is based in the firefox jsonview extension made by Ben Hollis: https://github.com/bhollis/jsonview/
*/
'use strict';

angular.module('gd.ui.jsonexplorer', [])
.directive('jsonExplorer', function ($http) {
	return {
		restrict: 'E',
		scope: {
			jsonData: '@',
			jsonUrl: '@',
			collapsed: '@'
		},
		link: function (scope, elem, attrs) {
			attrs.$observe('jsonUrl', function (val) {
				if (val) {
					$http.get(val)
					.success(function (data) {
						attrs.$set('jsonData', JSON.stringify(data));
					});
				}
			});
			attrs.$observe('jsonData', function (val) {
				var collapsed = attrs.collapsed == 'true' ? true : false;
				var output = '';
				var formatter = {};
				formatter.jsString = function (s) {
					var has = {
      					'\b': 'b',
      					'\f': 'f',
      					'\r': 'r',
      					'\n': 'n',
      					'\t': 't'
    				}, ws;
    				for (ws in has) {
      					if (-1 === s.indexOf(ws)) {
        					delete has[ws];
      					}
    				}

    				s = JSON.stringify({a:s});
    				s = s.slice(6, -2);
    				for (ws in has) {
      					s = s.replace(new RegExp('\\\\u000' + (ws.charCodeAt().toString(16)), 'ig'),
                    		'\\' + has[ws]);
    				}

    				return this.htmlEncode(s);
				};
				formatter.htmlEncode =  function (t) {
						if (t == null) {
							return '';
						}
    					return t.toString().replace(/&/g,"&")
    						.replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");
  					};
				formatter.decorateWithSpan = function (value, className) {
					return '' + this.htmlEncode(value) + '';
				};
				formatter.arrayToHtml = function (json) {
					var hasContents = false;
    				var output = '';
    				var numProps = 0;

    				for (var prop in json ) {
      					numProps++;
    				}

    				for (var prop in json) {
      					hasContents = true;
      					output += '
' + this.valueToHtml(json[prop]);
      					if (numProps > 1) {
       						output += ',';
      					}
      					output += '
';
      					numProps--;
    				}

    				if (hasContents) {
    					if (collapsed) {
    						output = '[
' + output + '
...]';
    					} else {
    						output = '[
' + output + '
...]';
    					}

    				} else {
      					output = '[ ]';
    				}
        			return output;
				};

				formatter.objectToHtml = function (json) {
					var hasContents = false;
    				var output = '';
    				var numProps = 0;
    				for (var prop in json ) {
      					numProps++;
    				}

    				for (var prop in json) {
      					hasContents = true;
      					output += '
' +
      					'"' + this.jsString(prop) +
                			'": ' + this.valueToHtml(json[prop]);
	      				if (numProps > 1) {
	        				output += ',';
	      				}
	      				output += '
';
	      				numProps--;
    				}

	    			if (hasContents) {
	    				if (collapsed) {
	    					if (JSON.stringify(json) != val) {
	    						output = '{
' + output + '
...}';
	    					} else {
	    						output = '{
' + output + '
...}';
	    					}
	      				} else {
	      					output = '{
' + output + '
}';
	      				}
	    			} else {
	      				output = '{ }';
	    			}

	    			return output;
				};

				formatter.valueToHtml = function (value) {
					var type = (value != null) && value.constructor;
					var output = '';

					if (value == null) {
						output += this.decorateWithSpan('null', 'null');
					}

					if (value && type == Array) {
						output += this.arrayToHtml(value);
					}

					if (value && type == Object) {
						output += this.objectToHtml(value);
					}

					if (type == Number) {
						output += this.decorateWithSpan(value, 'num');
					}

					if (type == String) {
						if (/^(http|https|file):\/\/[^\s]+$/i.test(value)) {
        					output += '"' +
        						this.jsString(value) + '"';
      					} else {
        					output += '"' + this.jsString(value) + '"';
      					}
					}

					if (type == Boolean) {
						output += this.decorateWithSpan(value, 'bool');
					}

					return output;
				};
				formatter.jsonToHtml = function (json) {
					return '
' + this.valueToHtml(json) + '
';
				};

				var json = JSON.parse(val);
				var x = formatter.jsonToHtml(json);
				elem.html(x);
				function collapse (evt) {
					var collapser = evt.target;
    				var target = collapser.parentNode.getElementsByClassName('collapsible');

    				if (!target.length) {
      					return;
    				}

    				target = target[0];

    				var ellipsis = target.parentNode.getElementsByClassName('ellipsis')[0];
    				if (target.style.display == 'none') {
				      target.style.display = '';
				      collapser.innerHTML = '-';
    				} else {

    				  target.style.display = 'none';
				      collapser.innerHTML = '+';
    				}
				}

				var collections = angular.element(elem)[0].getElementsByTagName('ul');
				for (var i = 0; i < collections.length; i++) {
					var collectionItem = collections[i];
					if (collectionItem.className.indexOf('collapsible') != -1) {
						if (collectionItem.parentNode.nodeName == 'LI') {
							var collapser = document.createElement('div');
							collapser.className = 'collapser';
							if (collapsed) {
								collapser.innerHTML = '+';
							} else {
								collapser.innerHTML = '-';
							}
							collapser.addEventListener('click', collapse, false);
							collectionItem.parentNode.insertBefore(collapser, collectionItem.parentNode.firstChild);
						}
					}
				}
			});
      	}
    }
});
<commit_msg>Fixing "Unknown provider" error when minify script https://coderwall.com/p/eeqo7q<commit_after>/*
Copyright (c) 2013, Goldark SS LTDA <http://www.goldark.co>
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
-Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
-Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
-Neither the name of the Goldark nor the names of its contributors may be used to endorse or promote products derived from this software
without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
*/

/*
This module is based in the firefox jsonview extension made by Ben Hollis: https://github.com/bhollis/jsonview/
*/
'use strict';

angular.module('gd.ui.jsonexplorer', [])
.directive('jsonExplorer', ['$http', function ($http) {
	return {
		restrict: 'E',
		scope: {
			jsonData: '@',
			jsonUrl: '@',
			collapsed: '@'
		},
		link: function (scope, elem, attrs) {
			attrs.$observe('jsonUrl', function (val) {
				if (val) {
					$http.get(val)
					.success(function (data) {
						attrs.$set('jsonData', JSON.stringify(data));
					});
				}
			});
			attrs.$observe('jsonData', function (val) {
				var collapsed = attrs.collapsed == 'true' ? true : false;
				var output = '';
				var formatter = {};
				formatter.jsString = function (s) {
					var has = {
      					'\b': 'b',
      					'\f': 'f',
      					'\r': 'r',
      					'\n': 'n',
      					'\t': 't'
    				}, ws;
    				for (ws in has) {
      					if (-1 === s.indexOf(ws)) {
        					delete has[ws];
      					}
    				}

    				s = JSON.stringify({a:s});
    				s = s.slice(6, -2);
    				for (ws in has) {
      					s = s.replace(new RegExp('\\\\u000' + (ws.charCodeAt().toString(16)), 'ig'),
                    		'\\' + has[ws]);
    				}

    				return this.htmlEncode(s);
				};
				formatter.htmlEncode =  function (t) {
						if (t == null) {
							return '';
						}
    					return t.toString().replace(/&/g,"&")
    						.replace(/"/g,""").replace(/</g,"<").replace(/>/g,">");
  					};
				formatter.decorateWithSpan = function (value, className) {
					return '' + this.htmlEncode(value) + '';
				};
				formatter.arrayToHtml = function (json) {
					var hasContents = false;
    				var output = '';
    				var numProps = 0;

    				for (var prop in json ) {
      					numProps++;
    				}

    				for (var prop in json) {
      					hasContents = true;
      					output += '
' + this.valueToHtml(json[prop]);
      					if (numProps > 1) {
       						output += ',';
      					}
      					output += '
';
      					numProps--;
    				}

    				if (hasContents) {
    					if (collapsed) {
    						output = '[
' + output + '
...]';
    					} else {
    						output = '[
' + output + '
...]';
    					}

    				} else {
      					output = '[ ]';
    				}
        			return output;
				};

				formatter.objectToHtml = function (json) {
					var hasContents = false;
    				var output = '';
    				var numProps = 0;
    				for (var prop in json ) {
      					numProps++;
    				}

    				for (var prop in json) {
      					hasContents = true;
      					output += '
' +
      					'"' + this.jsString(prop) +
                			'": ' + this.valueToHtml(json[prop]);
	      				if (numProps > 1) {
	        				output += ',';
	      				}
	      				output += '
';
	      				numProps--;
    				}

	    			if (hasContents) {
	    				if (collapsed) {
	    					if (JSON.stringify(json) != val) {
	    						output = '{
' + output + '
...}';
	    					} else {
	    						output = '{
' + output + '
...}';
	    					}
	      				} else {
	      					output = '{
' + output + '
}';
	      				}
	    			} else {
	      				output = '{ }';
	    			}

	    			return output;
				};

				formatter.valueToHtml = function (value) {
					var type = (value != null) && value.constructor;
					var output = '';

					if (value == null) {
						output += this.decorateWithSpan('null', 'null');
					}

					if (value && type == Array) {
						output += this.arrayToHtml(value);
					}

					if (value && type == Object) {
						output += this.objectToHtml(value);
					}

					if (type == Number) {
						output += this.decorateWithSpan(value, 'num');
					}

					if (type == String) {
						if (/^(http|https|file):\/\/[^\s]+$/i.test(value)) {
        					output += '"' +
        						this.jsString(value) + '"';
      					} else {
        					output += '"' + this.jsString(value) + '"';
      					}
					}

					if (type == Boolean) {
						output += this.decorateWithSpan(value, 'bool');
					}

					return output;
				};
				formatter.jsonToHtml = function (json) {
					return '
' + this.valueToHtml(json) + '
';
				};

				var json = JSON.parse(val);
				var x = formatter.jsonToHtml(json);
				elem.html(x);
				function collapse (evt) {
					var collapser = evt.target;
    				var target = collapser.parentNode.getElementsByClassName('collapsible');

    				if (!target.length) {
      					return;
    				}

    				target = target[0];

    				var ellipsis = target.parentNode.getElementsByClassName('ellipsis')[0];
    				if (target.style.display == 'none') {
				      target.style.display = '';
				      collapser.innerHTML = '-';
    				} else {

    				  target.style.display = 'none';
				      collapser.innerHTML = '+';
    				}
				}

				var collections = angular.element(elem)[0].getElementsByTagName('ul');
				for (var i = 0; i < collections.length; i++) {
					var collectionItem = collections[i];
					if (collectionItem.className.indexOf('collapsible') != -1) {
						if (collectionItem.parentNode.nodeName == 'LI') {
							var collapser = document.createElement('div');
							collapser.className = 'collapser';
							if (collapsed) {
								collapser.innerHTML = '+';
							} else {
								collapser.innerHTML = '-';
							}
							collapser.addEventListener('click', collapse, false);
							collectionItem.parentNode.insertBefore(collapser, collectionItem.parentNode.firstChild);
						}
					}
				}
			});
      	}
    }
}]);
<|endoftext|>