<commit_before>/*
 * Copyright (C) 2022 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import I18n from 'i18n!permissions_templates_28'
import {generateActionTemplates} from '../generateActionTemplates'

export const template = generateActionTemplates(
  [
    {
      title: I18n.t('LTI - add'),
      description: I18n.t('Allows user to manually add and delete an app in Account Settings.')
    },
    {
      description: I18n.t(
        'Allows user to add external app icons to the Rich Content Editor toolbar.'
      )
    },
    {
      description: I18n.t('Allows user to manually add and delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - delete'),
      description: I18n.t('Allows user to manually delete an app in Account Settings.')
    },
    {
      description: I18n.t('Allows user to manually add and delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - edit'),
      description: I18n.t('Allows user to edit configurations for manually added external apps.')
    }
  ],
  [
    {
      title: I18n.t('External Apps'),
      description: I18n.t(
        'If LTI - add is disabled, users can still install approved apps through the Canvas App Center (if enabled for your institution). However, if LTI - delete is not enabled, they cannot delete manually added external apps.'
      )
    }
  ],
  [
    {
      title: I18n.t('LTI - add'),
      description: I18n.t('Allows user to manually add and delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - delete'),
      description: I18n.t('Allows user to manually add and delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - edit'),
      description: I18n.t('Allows user to edit configurations for manually added external apps.')
    }
  ],
  [
    {
      title: I18n.t('External Apps'),
      description: I18n.t(
        'If LTI - add is disabled, users can still install approved apps through the Canvas App Center (if enabled for your institution). However, if LTI - delete is not enabled, they cannot delete manually added external apps.'
      )
    }
  ]
)
<commit_msg>treesame commit of origin/stable/2022-02-02<commit_after>/*
 * Copyright (C) 2022 - present Instructure, Inc.
 *
 * This file is part of Canvas.
 *
 * Canvas is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Affero General Public License as published by the Free
 * Software Foundation, version 3 of the License.
 *
 * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Affero General Public License along
 * with this program. If not, see <http://www.gnu.org/licenses/>.
 */

import I18n from 'i18n!permissions_templates_28'
import {generateActionTemplates} from '../generateActionTemplates'

export const template = generateActionTemplates(
  [
    {
      title: I18n.t('LTI - add'),
      description: I18n.t('Allows user to manually add an app in Account Settings.')
    },
    {
      description: I18n.t(
        'Allows user to add external app icons to the Rich Content Editor toolbar.'
      )
    },
    {
      description: I18n.t('Allows user to manually add an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - delete'),
      description: I18n.t('Allows user to manually delete an app in Account Settings.')
    },
    {
      description: I18n.t('Allows user to manually delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - edit'),
      description: I18n.t('Allows user to edit configurations for manually added external apps.')
    }
  ],
  [
    {
      title: I18n.t('External Apps'),
      description: I18n.t(
        'If LTI - add is disabled, users can still install approved apps through the Canvas App Center (if enabled for your institution). However, if LTI - delete is not enabled, they cannot delete manually added external apps.'
      )
    }
  ],
  [
    {
      title: I18n.t('LTI - add'),
      description: I18n.t('Allows user to manually add an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - delete'),
      description: I18n.t('Allows user to manually delete an app in Course Settings.')
    },
    {
      title: I18n.t('LTI - edit'),
      description: I18n.t('Allows user to edit configurations for manually added external apps.')
    }
  ],
  [
    {
      title: I18n.t('External Apps'),
      description: I18n.t(
        'If LTI - add is disabled, users can still install approved apps through the Canvas App Center (if enabled for your institution). However, if LTI - delete is not enabled, they cannot delete manually added external apps.'
      )
    }
  ]
)
<|endoftext|>


<commit_before>'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

module.exports = function(eventEmitter, secret) {
  var app = express();

  function signBlob(key, blob) {
    return 'sha1=' + crypto.createHmac('sha1', key).update(blob).digest('hex');
  }

  app.use(bodyParser.urlencoded({
    extended: true,
    verify: function(req, res, buffer) {
      if (!req.get('x-hub-signature'))
        throw new Error('No X-Hub-Signature found on request');

      if (!req.get('x-github-event'))
        throw new Error('No X-Github-Event found on request');

      var signature = req.get('x-hub-signature');
      var computedSignature = signBlob(secret, buffer);

      console.log('verified', signature, computedSignature);

      if (signature !== computedSignature) {
        console.warn('Recieved an invalid HMAC: calculated:' +
          computedSignature + ' != recieved:' + signature);
        throw new Error('Invalid Signature');
      }
    }
  }));

  app.get('/', (req, res) => {
    res.status(200).send('pong!');
  });

  app.post('/ghwebook', (req, res) => {
    const event = req.get('x-github-event');
    eventEmitter.emit(event, req.body.payload);
    res.status(200).send('ok');
  });

  return app;
};
<commit_msg>Parses the payload to json<commit_after>'use strict';

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');

module.exports = function(eventEmitter, secret) {
  var app = express();

  function signBlob(key, blob) {
    return 'sha1=' + crypto.createHmac('sha1', key).update(blob).digest('hex');
  }

  app.use(bodyParser.urlencoded({
    extended: true,
    verify: function(req, res, buffer) {
      if (!req.get('x-hub-signature'))
        throw new Error('No X-Hub-Signature found on request');

      if (!req.get('x-github-event'))
        throw new Error('No X-Github-Event found on request');

      var signature = req.get('x-hub-signature');
      var computedSignature = signBlob(secret, buffer);

      console.log('verified', signature, computedSignature);

      if (signature !== computedSignature) {
        console.warn('Recieved an invalid HMAC: calculated:' +
          computedSignature + ' != recieved:' + signature);
        throw new Error('Invalid Signature');
      }
    }
  }));

  app.get('/', (req, res) => {
    res.status(200).send('pong!');
  });

  app.post('/ghwebook', (req, res) => {
    const event = req.get('x-github-event');
    try {
      const payload = JSON.parse(req.body.payload);
      eventEmitter.emit(event, payload);
      res.status(200).send('ok');
    }
    catch (ex) {
      res.status(500).send(ex);
    }

  });

  return app;
};
<|endoftext|>


<commit_before>var Maki = require('maki');
var config = require('./config');
var decentral = new Maki( config );

var mongoose = decentral.mongoose;
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;

var Passport = require('maki-passport-local');
var passport = new Passport({ resource: 'Person' });

var Context = require('./lib/Context');
var context = new Context();

var Torrent = require('node-torrent-stream');
var readTorrent = require('read-torrent');
var magnet = require('magnet-uri');

var crypto = require('crypto');
var stream = require('stream');

decentral.use( passport );
decentral.use( context );

var Credit = new decentral.mongoose.Schema({
  _person: { type: ObjectId , ref: 'Person', required: true },
  role: { type: String , enum: ['host', 'producer', 'guest'] , required: true }
});

var Show = decentral.define('Show', {
  attributes: {
    name:    { type: String , max: 35 , required: true , slug: true },
    created: { type: Date , default: Date.now , required: true },
    description: { type: String },
    credits:     [ Credit ],
    donations: {
      type: { type: String , enum: ['bitcoin'] },
      destination: { type: String , max: 35 }
    }
  },
  icon: 'unmute'
});

var Recording = decentral.define('Recording', {
  attributes: {
    _show:    { type: ObjectId , ref: 'Show', required: false , alias: 'show' },
    title:    { type: String , max: 35 , required: true , slug: true },
    media:    { type: 'File', required: true },
    torrent:  { type: ObjectId , render: { create: false } },
    magnet:   { type: String , max: 24 , render: { create: false } },
    type:     { type: String , max: 5 , render: { create: false } },
    filename: { type: String , max: 255 , render: { create: false } },
    size:   { type: Number , render: { create: false } },
    recorded: { type: Date },
    released: { type: Date , default: Date.now , required: true },
    description: { type: String , format: 'markdown' },
    hash: { type: String , max: 32 , render: { create: false } },
    credits:  [ Credit ],
    // TODO: remove this in favor of a "Sources" object
    youtube: { type: String }
  },
  icon: 'sound'
});

var Checksum = decentral.define('Checksum', {
  attributes: {
    filename: { type: String },
    _file: { type: ObjectId },
    _recording: { type: ObjectId },
    hash: { type: String , max: 35 },
    type: { type: String , enum: ['md5'] },
    created: { type: Date , default: Date.now }
  },
  icon: 'lock'
});

Recording.post('query', function(next, done) {
  var recordings = this;
  Show.Model.populate( recordings , {
    path: '_show'
  }, function(err, populatedRecordings) {
    if (err) return done(err);
    return next( null , populatedRecordings );
  });
});

Recording.on('file:media', function(media) {
  console.log('received media:', media);

  var torrent = new Torrent({
    name: media.filename,
    trackers: config.torrents.trackers,
    webseeds: config.service.seeds.map(function(x) {
      return x + '/files/' + media._id
    })
  });
  var file = decentral.datastore.gfs.createReadStream({
    _id: media._id
  });
  var torrentstore = decentral.datastore.gfs.createWriteStream({
    mode: 'w',
    filename: media.filename + '.torrent',
    content_type: 'application/x-bittorrent'
  });
  torrentstore.on('error', function(data) {
    console.log('error!' , data );
  });
  torrentstore.on('close', function( torrentFile ) {
    readTorrent('http://localhost:15005/files/' + torrentFile._id , function(err, parsed) {
      if (err) console.error( err );
      var magnetURI = magnet.encode( parsed );

      Recording.patch({
        _id: media.metadata.document
      }, [
        { op: 'add', path: '/torrent' , value: torrentFile._id },
        { op: 'add', path: '/magnet' , value: magnetURI },
        { op: 'add', path: '/type' , value: media.contentType },
        { op: 'add', path: '/filename' , value: media.filename },
        { op: 'add', path: '/size' , value: media.length }
      ], function(err, num) {
        if (err) console.error( err );
        console.log('all done,', num , 'affected');
      });

    });
  });

  torrent.pipe( torrentstore );
  file.pipe( torrent );

});

Recording.pre('create', function(next, done) {
  var recording = this;
  if (!recording.media) return next();

  var db = decentral.datastore.mongoose.connections[0].db;
  var files = db.collection('fs.files');
  files.findOne({ _id: recording.media }, function(err, thing) {
    if (err) return done(err);

    return Checksum.create({
      filename: thing.filename,
      _file: thing._id,
      hash: thing.md5,
      type: 'md5'
    }, function() {
      recording.hash = thing.md5;
      return next();
    });
  });
});

var Person = decentral.define('Person', {
  attributes: {
    name: {
      given: { type: String , max: 30 },
      family: { type: String , max: 70 }
    },
    username: { type: String , max: 35 , slug: true },
    password: { type: String , max: 70 , masked: true },
    bio: { type: String , max: 240 }
  },
  virtuals: {
    'name.full': function() {
      return [ this.name.given , this.name.family ].join(' ');
    }
  },
  icon: 'user'
});

decentral.start(function() {

  decentral.app.get('/about', function(req, res, next) {
    return res.render('about');
  });

  // TODO: internalize to maki, provide sane defaults
  decentral.app.get('/recordings/:recordingSlug/edit', function(req, res, next) {
    Recording.get({ slug: req.param('recordingSlug') }, function(err, recording) {
      Show.Model.populate(recording, {
        path: '_show'
      }, function(err, recording) {
        if (!req.user) return res.error(404);
        if (!req.user.can('edit', recording)) return res.error(404);
        return res.render('recording-edit', {
          item: recording
        });
      });
    });
  });

  decentral.app.post('/recordings/:recordingSlug', function(req, res, next) {
    Recording.update({ slug: req.param('recordingSlug') }, req.body , function(err, recording) {
      Show.Model.populate(recording, {
        path: '_show'
      }, function(err, recording) {
        if (!req.user) return res.error(404);
        if (!req.user.can('edit', recording)) return res.error(404);
        req.flash('success', 'Content edited successfully!');
        return res.redirect('/recordings/' + req.param('recordingSlug'));
      });
    });
  });

  decentral.app.get('/search', function(req, res, next) {
    Show.query({}, function(err, shows) {
      return res.send({
        results: shows.map(function(x) {
          return {
            title: x.name
          }
        })
      });
    });
  });
});
<commit_msg>Add new soundcloud attribute.<commit_after>var Maki = require('maki');
var config = require('./config');
var decentral = new Maki( config );

var mongoose = decentral.mongoose;
var Schema = mongoose.Schema;
var ObjectId = Schema.Types.ObjectId;

var Passport = require('maki-passport-local');
var passport = new Passport({ resource: 'Person' });

var Context = require('./lib/Context');
var context = new Context();

var Torrent = require('node-torrent-stream');
var readTorrent = require('read-torrent');
var magnet = require('magnet-uri');

var crypto = require('crypto');
var stream = require('stream');

decentral.use( passport );
decentral.use( context );

var Credit = new decentral.mongoose.Schema({
  _person: { type: ObjectId , ref: 'Person', required: true },
  role: { type: String , enum: ['host', 'producer', 'guest'] , required: true }
});

var Show = decentral.define('Show', {
  attributes: {
    name:    { type: String , max: 35 , required: true , slug: true },
    created: { type: Date , default: Date.now , required: true },
    description: { type: String },
    credits:     [ Credit ],
    donations: {
      type: { type: String , enum: ['bitcoin'] },
      destination: { type: String , max: 35 }
    }
  },
  icon: 'unmute'
});

var Recording = decentral.define('Recording', {
  attributes: {
    _show:    { type: ObjectId , ref: 'Show', required: false , alias: 'show' },
    title:    { type: String , max: 35 , required: true , slug: true },
    media:    { type: 'File', required: true },
    torrent:  { type: ObjectId , render: { create: false } },
    magnet:   { type: String , max: 24 , render: { create: false } },
    type:     { type: String , max: 5 , render: { create: false } },
    filename: { type: String , max: 255 , render: { create: false } },
    size:   { type: Number , render: { create: false } },
    recorded: { type: Date },
    released: { type: Date , default: Date.now , required: true },
    description: { type: String , format: 'markdown' },
    hash: { type: String , max: 32 , render: { create: false } },
    credits:  [ Credit ],
    // TODO: remove these in favor of a "Sources" object
    youtube: { type: String },
    soundcloud: { type: String },
  },
  icon: 'sound'
});

var Checksum = decentral.define('Checksum', {
  attributes: {
    filename: { type: String },
    _file: { type: ObjectId },
    _recording: { type: ObjectId },
    hash: { type: String , max: 35 },
    type: { type: String , enum: ['md5'] },
    created: { type: Date , default: Date.now }
  },
  icon: 'lock'
});

Recording.post('query', function(next, done) {
  var recordings = this;
  Show.Model.populate( recordings , {
    path: '_show'
  }, function(err, populatedRecordings) {
    if (err) return done(err);
    return next( null , populatedRecordings );
  });
});

Recording.on('file:media', function(media) {
  console.log('received media:', media);

  var torrent = new Torrent({
    name: media.filename,
    trackers: config.torrents.trackers,
    webseeds: config.service.seeds.map(function(x) {
      return x + '/files/' + media._id
    })
  });
  var file = decentral.datastore.gfs.createReadStream({
    _id: media._id
  });
  var torrentstore = decentral.datastore.gfs.createWriteStream({
    mode: 'w',
    filename: media.filename + '.torrent',
    content_type: 'application/x-bittorrent'
  });
  torrentstore.on('error', function(data) {
    console.log('error!' , data );
  });
  torrentstore.on('close', function( torrentFile ) {
    readTorrent('http://localhost:15005/files/' + torrentFile._id , function(err, parsed) {
      if (err) console.error( err );
      var magnetURI = magnet.encode( parsed );

      Recording.patch({
        _id: media.metadata.document
      }, [
        { op: 'add', path: '/torrent' , value: torrentFile._id },
        { op: 'add', path: '/magnet' , value: magnetURI },
        { op: 'add', path: '/type' , value: media.contentType },
        { op: 'add', path: '/filename' , value: media.filename },
        { op: 'add', path: '/size' , value: media.length }
      ], function(err, num) {
        if (err) console.error( err );
        console.log('all done,', num , 'affected');
      });

    });
  });

  torrent.pipe( torrentstore );
  file.pipe( torrent );

});

Recording.pre('create', function(next, done) {
  var recording = this;
  if (!recording.media) return next();

  var db = decentral.datastore.mongoose.connections[0].db;
  var files = db.collection('fs.files');
  files.findOne({ _id: recording.media }, function(err, thing) {
    if (err) return done(err);

    return Checksum.create({
      filename: thing.filename,
      _file: thing._id,
      hash: thing.md5,
      type: 'md5'
    }, function() {
      recording.hash = thing.md5;
      return next();
    });
  });
});

var Person = decentral.define('Person', {
  attributes: {
    name: {
      given: { type: String , max: 30 },
      family: { type: String , max: 70 }
    },
    username: { type: String , max: 35 , slug: true },
    password: { type: String , max: 70 , masked: true },
    bio: { type: String , max: 240 }
  },
  virtuals: {
    'name.full': function() {
      return [ this.name.given , this.name.family ].join(' ');
    }
  },
  icon: 'user'
});

decentral.start(function() {

  decentral.app.get('/about', function(req, res, next) {
    return res.render('about');
  });

  // TODO: internalize to maki, provide sane defaults
  decentral.app.get('/recordings/:recordingSlug/edit', function(req, res, next) {
    Recording.get({ slug: req.param('recordingSlug') }, function(err, recording) {
      Show.Model.populate(recording, {
        path: '_show'
      }, function(err, recording) {
        if (!req.user) return res.error(404);
        if (!req.user.can('edit', recording)) return res.error(404);
        return res.render('recording-edit', {
          item: recording
        });
      });
    });
  });

  decentral.app.post('/recordings/:recordingSlug', function(req, res, next) {
    Recording.update({ slug: req.param('recordingSlug') }, req.body , function(err, recording) {
      Show.Model.populate(recording, {
        path: '_show'
      }, function(err, recording) {
        if (!req.user) return res.error(404);
        if (!req.user.can('edit', recording)) return res.error(404);
        req.flash('success', 'Content edited successfully!');
        return res.redirect('/recordings/' + req.param('recordingSlug'));
      });
    });
  });

  decentral.app.get('/search', function(req, res, next) {
    Show.query({}, function(err, shows) {
      return res.send({
        results: shows.map(function(x) {
          return {
            title: x.name
          }
        })
      });
    });
  });
});
<|endoftext|>


<commit_before>const AmazonS3URI = require("amazon-s3-uri")
const AWS = require("aws-sdk")
const axios = require("axios")
const FileType = require("file-type")
const fs = require("fs")
const path = require("path")
const pLimit = require("p-limit")
const readdir = require("recursive-readdir")
const replaceExt = require("replace-ext")
const rmfr = require("rmfr")
const sharp = require("sharp")
const xml2js = require("xml2js")

const ROOT_PATH = "/tmp"
const TILE_FORMAT = {
  jpg: "image/jpeg",
  png: "image/png",
}

const s3Client = new AWS.S3({ apiVersion: "2006-03-01" })
const limit = pLimit(10)

exports.handler = async ({ contentURL }) => {
  if (!contentURL) {
    return {
      status: 400,
      body:
        "Please specify a content URL to process, i.e. http://www.zoomhub.net/v1/content/:id",
    }
  }

  try {
    await processContent({ contentURL })
  } catch (error) {
    const isClientError = error instanceof TypeError

    await markAsFailure({ contentURL, error: error.message })

    return {
      status: isClientError ? 400 : 500,
      body: error.message,
    }
  }

  return {
    status: 200,
    body: "success",
  }
}

// Public API
const processContent = async ({ contentURL }) => {
  const content = await fetchContent(contentURL)
  if (!content.id) {
    throw new TypeError("Invalid content")
  }

  const contentId = parseContentId(content.id)
  if (!contentId) {
    throw new TypeError(`Invalid content ID: ${contentId}`)
  }

  const sourceURL = content.url
  const s3URL = parseS3URL(sourceURL)
  log("meta", { contentId, contentURL, sourceURL, s3URL })

  const body = s3URL
    ? await fetchS3URL({ ...s3URL, s3Client })
    : await fetchGenericURL(sourceURL)

  const outputPath = `${ROOT_PATH}/${contentId}`
  log("output", { outputPath })

  // Clean up output directory
  await Promise.all([
    rmfr(outputPath),
    rmfr(`${outputPath}_files`),
    rmfr(`${outputPath}.dzi`),
  ])

  // Write source file
  await fs.promises.writeFile(outputPath, body)

  const fileType = await FileType.fromFile(outputPath)
  const isPNG = fileType && fileType.mime === "image/png"
  const tileFormat = isPNG ? { id: "png" } : { id: "jpg", quality: 90 }
  log("file meta", { fileType, isPNG, tileFormat })

  await sharp(outputPath, { limitInputPixels: false })
    .toFormat(tileFormat)
    .tile({
      depth: "onepixel",
      layout: "dz",
      overlap: 1,
      size: 254,
    })
    .toFile(`${outputPath}.dz`)

  const dziXMLString = await fs.promises.readFile(`${outputPath}.dzi`)
  const fixedDZIXMLString = await fixDZITileFormat(dziXMLString)
  await fs.promises.writeFile(`${outputPath}.dzi`, fixedDZIXMLString)

  const [fileSize, dzi] = await Promise.all([
    (await fs.promises.stat(outputPath)).size,
    await parseDZI(fixedDZIXMLString),
  ])

  await uploadDZI({
    basePath: outputPath,
    s3Client,
    tileFormat,
  })

  await markAsSuccess({
    contentURL,
    mime: fileType && fileType.mime,
    size: fileSize,
    dzi,
  })
}

// Helpers
const parseS3URL = (url) => {
  try {
    return AmazonS3URI(url)
  } catch (error) {
    // ignore errors
    return null
  }
}

const parseContentId = (value) => {
  const isValid = /[a-zA-Z0-9]+/.test(value)
  if (!isValid) {
    return null
  }

  return value
}

const log = (message, props = {}) =>
  console.log({ time: new Date().toISOString(), message, ...props })

const fetchContent = async (url) => (await axios.get(url)).data
const fetchS3URL = async ({ s3Client, bucket, key }) =>
  (await s3Client.getObject({ Bucket: bucket, Key: key }).promise()).Body
const fetchGenericURL = async (url) =>
  (await axios.get(url, { responseType: "arraybuffer" })).data

const uploadDZI = async ({ s3Client, basePath, tileFormat }) => {
  const tileFileNames = await readdir(`${basePath}_files`)
  const manifestFileName = `${basePath}.dzi`
  log("DZI", { manifestFileName, tileFileNames })

  const tileOperations = tileFileNames.map((fileName) =>
    limit(() => {
      const baseKey = path.relative(ROOT_PATH, fileName)
      const key =
        tileFormat.id === "jpg" ? replaceExt(baseKey, ".jpg") : baseKey
      return uploadFile({
        s3Client,
        fileName,
        key,
        contentType: TILE_FORMAT[tileFormat.id],
      })
    })
  )

  const dziManifestOperation = limit(() =>
    uploadFile({
      s3Client,
      fileName: manifestFileName,
      key: path.relative(ROOT_PATH, manifestFileName),
      contentType: "application/xml",
    })
  )

  return Promise.all([...tileOperations, dziManifestOperation])
}

const uploadFile = async ({ s3Client, fileName, key, contentType }) => {
  await s3Client
    .upload({
      ACL: "public-read",
      Body: fs.createReadStream(fileName),
      Bucket: process.env["S3_CACHE_BUCKET"],
      ContentType: contentType,
      Key: `content/${key}`,
    })
    .promise()

  log("file uploaded", { fileName, key, contentType })
}

const markAsSuccess = async ({ contentURL, mime, size, dzi }) => {
  const type = "success"
  await axios.put(`${contentURL}/completion`, {
    type,
    mime,
    size,
    dzi,
  })
  log("content completed", { type, contentURL, mime, size, dzi })
}

const markAsFailure = async ({ contentURL, error }) => {
  const type = "failure"
  await axios.post(`${contentURL}/completion`, { type, error })
  log("content completed", { type, contentURL, error })
}

const parseDZI = async (xmlString) => {
  const dzi = await xml2js.parseStringPromise(xmlString)
  const image = dzi.Image["$"]
  const size = dzi.Image.Size[0]["$"]
  return {
    width: parseInt(size.Width, 10),
    height: parseInt(size.Height, 10),
    tileFormat: image.Format,
    tileOverlap: parseInt(image.Overlap, 10),
    tileSize: parseInt(image.TileSize, 10),
  }
}

// Renames `Format="jpeg"` to `Format="jpg"` for backwards compatibility
// See: https://github.com/lovell/sharp/issues/1206
const fixDZITileFormat = async (xmlString) => {
  const dzi = await xml2js.parseStringPromise(xmlString)
  const format = dzi.Image["$"].Format
  const fixedFormat = format === "jpeg" ? "jpg" : format
  dzi.Image["$"].Format = fixedFormat

  return new xml2js.Builder().buildObject(dzi)
}
<commit_msg>Do not process content twice<commit_after>const AmazonS3URI = require("amazon-s3-uri")
const AWS = require("aws-sdk")
const axios = require("axios")
const FileType = require("file-type")
const fs = require("fs")
const path = require("path")
const pLimit = require("p-limit")
const readdir = require("recursive-readdir")
const replaceExt = require("replace-ext")
const rmfr = require("rmfr")
const sharp = require("sharp")
const xml2js = require("xml2js")

const ROOT_PATH = "/tmp"
const TILE_FORMAT = {
  jpg: "image/jpeg",
  png: "image/png",
}

const s3Client = new AWS.S3({ apiVersion: "2006-03-01" })
const limit = pLimit(10)

exports.handler = async ({ contentURL }) => {
  if (!contentURL) {
    return {
      status: 400,
      body:
        "Please specify a content URL to process, i.e. http://www.zoomhub.net/v1/content/:id",
    }
  }

  try {
    await processContent({ contentURL })
  } catch (error) {
    const isClientError = error instanceof TypeError

    await markAsFailure({ contentURL, error: error.message })

    return {
      status: isClientError ? 400 : 500,
      body: error.message,
    }
  }

  return {
    status: 200,
    body: "success",
  }
}

// Public API
const processContent = async ({ contentURL }) => {
  const content = await fetchContent(contentURL)
  if (!content.id) {
    throw new TypeError("Invalid content")
  }

  const contentId = parseContentId(content.id)
  if (!contentId) {
    throw new TypeError(`Invalid content ID: ${contentId}`)
  }

  if (content.ready) {
    throw new TypeError(`Content already processed`)
  }

  const sourceURL = content.url
  const s3URL = parseS3URL(sourceURL)
  log("meta", { contentId, contentURL, sourceURL, s3URL })

  const body = s3URL
    ? await fetchS3URL({ ...s3URL, s3Client })
    : await fetchGenericURL(sourceURL)

  const outputPath = `${ROOT_PATH}/${contentId}`
  log("output", { outputPath })

  // Clean up output directory
  await Promise.all([
    rmfr(outputPath),
    rmfr(`${outputPath}_files`),
    rmfr(`${outputPath}.dzi`),
  ])

  // Write source file
  await fs.promises.writeFile(outputPath, body)

  const fileType = await FileType.fromFile(outputPath)
  const isPNG = fileType && fileType.mime === "image/png"
  const tileFormat = isPNG ? { id: "png" } : { id: "jpg", quality: 90 }
  log("file meta", { fileType, isPNG, tileFormat })

  await sharp(outputPath, { limitInputPixels: false })
    .toFormat(tileFormat)
    .tile({
      depth: "onepixel",
      layout: "dz",
      overlap: 1,
      size: 254,
    })
    .toFile(`${outputPath}.dz`)

  const dziXMLString = await fs.promises.readFile(`${outputPath}.dzi`)
  const fixedDZIXMLString = await fixDZITileFormat(dziXMLString)
  await fs.promises.writeFile(`${outputPath}.dzi`, fixedDZIXMLString)

  const [fileSize, dzi] = await Promise.all([
    (await fs.promises.stat(outputPath)).size,
    await parseDZI(fixedDZIXMLString),
  ])

  await uploadDZI({
    basePath: outputPath,
    s3Client,
    tileFormat,
  })

  await markAsSuccess({
    contentURL,
    mime: fileType && fileType.mime,
    size: fileSize,
    dzi,
  })
}

// Helpers
const parseS3URL = (url) => {
  try {
    return AmazonS3URI(url)
  } catch (error) {
    // ignore errors
    return null
  }
}

const parseContentId = (value) => {
  const isValid = /[a-zA-Z0-9]+/.test(value)
  if (!isValid) {
    return null
  }

  return value
}

const log = (message, props = {}) =>
  console.log({ time: new Date().toISOString(), message, ...props })

const fetchContent = async (url) => (await axios.get(url)).data
const fetchS3URL = async ({ s3Client, bucket, key }) =>
  (await s3Client.getObject({ Bucket: bucket, Key: key }).promise()).Body
const fetchGenericURL = async (url) =>
  (await axios.get(url, { responseType: "arraybuffer" })).data

const uploadDZI = async ({ s3Client, basePath, tileFormat }) => {
  const tileFileNames = await readdir(`${basePath}_files`)
  const manifestFileName = `${basePath}.dzi`
  log("DZI", { manifestFileName, tileFileNames })

  const tileOperations = tileFileNames.map((fileName) =>
    limit(() => {
      const baseKey = path.relative(ROOT_PATH, fileName)
      const key =
        tileFormat.id === "jpg" ? replaceExt(baseKey, ".jpg") : baseKey
      return uploadFile({
        s3Client,
        fileName,
        key,
        contentType: TILE_FORMAT[tileFormat.id],
      })
    })
  )

  const dziManifestOperation = limit(() =>
    uploadFile({
      s3Client,
      fileName: manifestFileName,
      key: path.relative(ROOT_PATH, manifestFileName),
      contentType: "application/xml",
    })
  )

  return Promise.all([...tileOperations, dziManifestOperation])
}

const uploadFile = async ({ s3Client, fileName, key, contentType }) => {
  await s3Client
    .upload({
      ACL: "public-read",
      Body: fs.createReadStream(fileName),
      Bucket: process.env["S3_CACHE_BUCKET"],
      ContentType: contentType,
      Key: `content/${key}`,
    })
    .promise()

  log("file uploaded", { fileName, key, contentType })
}

const markAsSuccess = async ({ contentURL, mime, size, dzi }) => {
  const type = "success"
  await axios.put(`${contentURL}/completion`, {
    type,
    mime,
    size,
    dzi,
  })
  log("content completed", { type, contentURL, mime, size, dzi })
}

const markAsFailure = async ({ contentURL, error }) => {
  const type = "failure"
  await axios.post(`${contentURL}/completion`, { type, error })
  log("content completed", { type, contentURL, error })
}

const parseDZI = async (xmlString) => {
  const dzi = await xml2js.parseStringPromise(xmlString)
  const image = dzi.Image["$"]
  const size = dzi.Image.Size[0]["$"]
  return {
    width: parseInt(size.Width, 10),
    height: parseInt(size.Height, 10),
    tileFormat: image.Format,
    tileOverlap: parseInt(image.Overlap, 10),
    tileSize: parseInt(image.TileSize, 10),
  }
}

// Renames `Format="jpeg"` to `Format="jpg"` for backwards compatibility
// See: https://github.com/lovell/sharp/issues/1206
const fixDZITileFormat = async (xmlString) => {
  const dzi = await xml2js.parseStringPromise(xmlString)
  const format = dzi.Image["$"].Format
  const fixedFormat = format === "jpeg" ? "jpg" : format
  dzi.Image["$"].Format = fixedFormat

  return new xml2js.Builder().buildObject(dzi)
}
<|endoftext|>


<commit_before>WorldMorph.prototype.Arduino.firmata = firmata;

WorldMorph.prototype.Arduino.getSerialPorts = function (callback) {
    var myself = this,
    portList = [];

    bluetoothSerial.list(function (devices) {
        devices.forEach(function (device) {
            if (!myself.isPortLocked(device.address)) {
                portList[device.name] = device.address;
            }
        });
        callback(portList);
    },
    function () {
        myself.alert('Could not get device list.');
    });
};

// Reverting some changes
WorldMorph.prototype.init = WorldMorph.prototype.originalInit;

// Pinch-zoom and pan implementation
HandMorph.prototype.originalInit = HandMorph.prototype.init;
HandMorph.prototype.init = function (aWorld) {
    this.originalInit(aWorld);
    this.zoom = Number(document.body.style.zoom || 1);
    this.translation = new Point(0, 0);
    this.translateStyle = '0px, 0px';
};

HandMorph.prototype.originalProcessTouchStart = HandMorph.prototype.processTouchStart;
HandMorph.prototype.processTouchStart = function (event) {
    if (event.touches.length === 2) {
        this.pinchDistance = 0;
    } else if (event.touches.length === 3) {
        this.translateStartPoint = (new Point(event.touches[0].pageX, event.touches[0].pageY)).subtract(this.translation);
    }
    this.originalProcessTouchStart(event);
};

HandMorph.prototype.originalProcessTouchMove = HandMorph.prototype.processTouchMove;
HandMorph.prototype.processTouchMove = function (event) {
    if (event.touches.length === 2) {
        var distance =
            Math.sqrt(
                    Math.pow(event.touches[0].pageX - event.touches[1].pageX, 2) +
                    Math.pow(event.touches[0].pageY - event.touches[1].pageY, 2));
        if (distance > this.pinchDistance) {
            this.zoom = Math.min(this.zoom + distance * 0.0001 , 3);
        } else {
            this.zoom = Math.max(this.zoom - distance * 0.0001, 1);
        }
        worldElement.style.transform = 'scale(' + this.zoom + ') translate(' + this.translateStyle + ')';
        this.pinchDistance = distance;
    } else if (event.touches.length === 3) {
        this.translation = (new Point(event.touches[0].pageX, event.touches[0].pageY)).subtract(this.translateStartPoint);
        this.translateStyle = this.translation.x + 'px, ' + this.translation.y + 'px';
        worldElement.style.transform = 'scale(' + this.zoom + ') translate(' + this.translateStyle + ')';
    }
    this.originalProcessTouchMove(event);
};

<commit_msg>dragging in mobile finally works<commit_after>WorldMorph.prototype.Arduino.firmata = firmata;

WorldMorph.prototype.Arduino.getSerialPorts = function (callback) {
    var myself = this,
    portList = [];

    bluetoothSerial.list(function (devices) {
        devices.forEach(function (device) {
            if (!myself.isPortLocked(device.address)) {
                portList[device.name] = device.address;
            }
        });
        callback(portList);
    },
    function () {
        myself.alert('Could not get device list.');
    });
};

// Reverting some changes
WorldMorph.prototype.init = WorldMorph.prototype.originalInit;

// Pinch-zoom and pan implementation
HandMorph.prototype.originalInit = HandMorph.prototype.init;
HandMorph.prototype.init = function (aWorld) {
    this.originalInit(aWorld);
    this.zoom = Number(document.body.style.zoom || 1);
    this.translation = new Point(0, 0);
    this.translateStyle = '0px, 0px';
};

HandMorph.prototype.processTouchStart = function (event) {
    var myself = this;

    MorphicPreferences.isTouchDevice = true;
    clearInterval(this.touchHoldTimeout);

    if (event.touches.length === 1) {
        this.touchHoldTimeout = setInterval( // simulate mouseRightClick
            function () {
                myself.processMouseDown({button: 2});
                myself.processMouseUp({button: 2});
                event.preventDefault();
                clearInterval(myself.touchHoldTimeout);
            },
            400
        );
        this.processMouseMove(event.touches[0]); // update my position
        this.processMouseDown({button: 0});
        event.preventDefault();
    } else if (event.touches.length === 2) {
        this.pinchDistance = 0;
    } else if (event.touches.length === 3) {
        this.translateStartPoint = (new Point(event.touches[0].pageX, event.touches[0].pageY)).subtract(this.translation);
    }
};

HandMorph.prototype.processTouchMove = function (event) {
    MorphicPreferences.isTouchDevice = true;

    if (event.touches.length === 1) {
        this.processMouseMove(event.touches[0]);
        clearInterval(this.touchHoldTimeout);
    } else if (event.touches.length === 2) {
        var distance =
            Math.sqrt(
                    Math.pow(event.touches[0].pageX - event.touches[1].pageX, 2) +
                    Math.pow(event.touches[0].pageY - event.touches[1].pageY, 2));
        if (distance > this.pinchDistance) {
            this.zoom = Math.min(this.zoom + distance * 0.0001 , 3);
        } else {
            this.zoom = Math.max(this.zoom - distance * 0.0001, 1);
        }
        this.pinchDistance = distance;
        worldElement.style.transform = 'scale(' + this.zoom + ');
    } else if (event.touches.length === 3) {
        this.translation = (new Point(event.touches[0].pageX, event.touches[0].pageY)).subtract(this.translateStartPoint);
        worldElement.style.left = this.translation.x + 'px';
        worldElement.style.top = this.translation.y + 'px';
    }
};

// Need to factor zoom in here:
function getDocumentPositionOf(aDOMelement) {
    // answer the absolute coordinates of a DOM element in the document
    var pos, offsetParent;
    if (aDOMelement === null) {
        return {x: 0, y: 0};
    }
    pos = {x: aDOMelement.offsetLeft, y: aDOMelement.offsetTop};
    offsetParent = aDOMelement.offsetParent;
    while (offsetParent !== null) {
        pos.x += offsetParent.offsetLeft;
        pos.y += offsetParent.offsetTop;
        if (offsetParent !== document.body &&
                offsetParent !== document.documentElement) {
            pos.x -= offsetParent.scrollLeft;
            pos.y -= offsetParent.scrollTop;
        }
        offsetParent = offsetParent.offsetParent;
    }
    return pos;
}

<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Content extends Component {
  static displayName = 'Content';

  static propTypes = {
    backgroundColor: PropTypes.objectOf(PropTypes.string),
    output: PropTypes.arrayOf(PropTypes.element),
    prompt: PropTypes.objectOf(PropTypes.string),
    inputStyles: PropTypes.objectOf(PropTypes.string),
    handleChange: PropTypes.func,
    handlerKeyPress: PropTypes.func.isRequired
  };

  static contextTypes = {
    symbol: PropTypes.string,
    maximise: PropTypes.bool
  };

  componentDidMount = () => {
    this.focusInput();
  };

  // Adjust scrolling
  componentDidUpdate = () => {
    if (this.inputWrapper !== null) this.inputWrapper.scrollIntoView(false);
  };

  focusInput = () => {
    if (this.com !== null) this.com.focus();
  };

  render() {
    const {
      output,
      prompt,
      inputStyles,
      handleChange,
      backgroundColor,
      handlerKeyPress
    } = this.props;
    const { symbol, maximise } = this.context;

    return (

              {output}

 (this.inputWrapper = elm)}
              >
                {symbol}
                <input
                  className="terminal-main-input"
                  style={inputStyles}
                  type="text"
                  tabIndex="-1"
                  ref={com => (this.com = com)}
                  onKeyPress={handleChange}
                  onKeyDown={e => handlerKeyPress(e, this.com)}
                />

    );
  }
}

export default Content;<commit_msg>updated<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Content extends Component {
  static displayName = 'Content';

  static propTypes = {
    backgroundColor: PropTypes.objectOf(PropTypes.string),
    output: PropTypes.arrayOf(PropTypes.element),
    prompt: PropTypes.objectOf(PropTypes.string),
    inputStyles: PropTypes.objectOf(PropTypes.string),
    handleChange: PropTypes.func,
    handlerKeyPress: PropTypes.func.isRequired,
  };

  static contextTypes = {
    symbol: PropTypes.string,
    maximise: PropTypes.bool,
  };

  componentDidMount = () => {
    this.focusInput();
  };

  // Adjust scrolling
  componentDidUpdate = () => {
    if (this.inputWrapper !== null) this.inputWrapper.scrollIntoView(false);
  };

  focusInput = () => {
    if (this.com !== null) this.com.focus();
  };

  render() {
    const {
      output,
      prompt,
      inputStyles,
      handleChange,
      backgroundColor,
      handlerKeyPress,
    } = this.props;
    const { symbol, maximise } = this.context;

    return (

              {output}

 (this.inputWrapper = elm)}
              >
                {symbol}
                <input
                  className="terminal-main-input"
                  style={inputStyles}
                  type="text"
                  tabIndex="-1"
                  ref={com => (this.com = com)}
                  onKeyPress={handleChange}
                  onKeyDown={e => handlerKeyPress(e, this.com)}
                />

    );
  }
}

export default Content;
<|endoftext|>


<commit_before>import { vec3, vec4, mat4, quat, utils } from "pex-math";

let currentOutputVec3 = vec3.create();
let currentOutputQuat = quat.create();

function updateAnimation(animation, deltaTime) {
  // console.log('updateAnimation', animation)
  // if (!animation.enabled) return;

  if (!animation.prevTime) {
    animation.time = 0;
    animation.prevTime = Date.now();
    animation.playing = true;
  }

  if (animation.playing) {
    const animationLength =
      animation.duration ||
      animation.channels[0].input[animation.channels[0].input.length - 1];
    const now = Date.now();
    // const deltaTime = (now - animation.prevTime) / 1000;

    animation.prevTime = now;
    animation.time += deltaTime;
    if (animation.time > animationLength) {
      if (animation.loop) {
        animation.time %= animationLength;
      } else {
        animation.time = 0;
        animation.playing = false;
        // animation.set({ playing: false });
      }
    }

    animation.needsUpdate = true;
  }

  // console.log(animation)
  if (!animation.needsUpdate) return;

  animation.needsUpdate = false;

  for (let i = 0; i < animation.channels.length; i++) {
    const channel = animation.channels[i];
    const inputData = channel.input;

    let prevIndex;
    let nextIndex;

    for (let j = 0; j < inputData.length; j++) {
      nextIndex = j;
      if (inputData[j] >= animation.time) {
        break;
      }
      prevIndex = nextIndex;
    }

    const isRotation = channel.path === "rotation";
    const outputData = channel.output;
    const prevInput = inputData[prevIndex];
    const nextInput = inputData[nextIndex];
    const scale = nextInput - prevInput || 1;

    const t = (animation.time - prevInput) / scale;

    // console.log('t', t)

    if (prevIndex !== undefined) {
      switch (channel.interpolation) {
        case "STEP":
          if (isRotation) {
            quat.set(currentOutputQuat, outputData[prevIndex]);
          } else {
            vec3.set(currentOutputVec3, outputData[prevIndex]);
          }
          break;
        case "CUBICSPLINE": {
          const vec = isRotation ? vec4 : vec3;
          const tt = t * t;
          const ttt = tt * t;

          // Each input value corresponds to three output values of the same type: in-tangent, data point, and out-tangent.
          // p0
          const prevPosition = vec.copy(outputData[prevIndex * 3 + 1]);

          // p1
          const nextPos = vec.copy(outputData[nextIndex * 3 + 1]);

          // m0 = (tk+1 - tk)bk
          const prevOutTangent = prevIndex
            ? vec.scale(vec.copy(outputData[prevIndex * 3 + 2]), scale)
            : vec.create();

          // m1 = (tk+1 - tk)ak+1
          const nextInTangent =
            nextIndex !== inputData.length - 1
              ? vec.scale(vec.copy(outputData[prevIndex * 3]), scale)
              : vec.create();

          // p(t) = (2t³ - 3t² + 1)p0 + (t³ - 2t² + t)m0 + (-2t³ + 3t²)p1 + (t³ - t²)m1
          const p0 = vec.scale(prevPosition, 2 * ttt - 3 * tt + 1);
          const m0 = vec.scale(prevOutTangent, ttt - 2 * tt + t);
          const p1 = vec.scale(nextPos, -2 * ttt + 3 * tt);
          const m1 = vec.scale(nextInTangent, ttt - tt);

          if (isRotation) {
            quat.set(
              currentOutputQuat,
              quat.normalize([
                p0[0] + m0[0] + p1[0] + m1[0],
                p0[1] + m0[1] + p1[1] + m1[1],
                p0[2] + m0[2] + p1[2] + m1[2],
                p0[3] + m0[3] + p1[3] + m1[3],
              ])
            );
          } else {
            vec3.set(
              currentOutputVec3,
              vec3.add(vec3.add(vec3.add(p0, m0), p1), m1)
            );
          }

          break;
        }
        default:
          // LINEAR
          if (isRotation) {
            quat.slerp(
              quat.set(currentOutputQuat, outputData[prevIndex]),
              outputData[nextIndex],
              t
            );
            // console.log(
            //   "currentOutputQuat",
            //   // prevIndex,
            //   // outputData[prevIndex],
            //   // nextIndex,
            //   // outputData[nextIndex],
            //   t,
            //   currentOutputQuat
            // );
          } else {
            vec3.set(
              currentOutputVec3,
              outputData[nextIndex].map((output, index) =>
                utils.lerp(outputData[prevIndex][index], output, t)
              )
            );
          }
      }

      if (isRotation) {
        quat.set(channel.target.transform.rotation, currentOutputQuat);
        channel.target.transform.dirty = true;
        // console.log(
        //   "channel.target.transform.rotation",
        //   channel.target.transform.rotation
        // );
        window.transform = channel.target.transform;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };
        // channel.target.transform.set({
        //   rotation: quat.copy(currentOutputQuat),
        // });
      } else if (channel.path === "translation") {
        vec3.set(channel.target.transform.position, currentOutputVec3);
        channel.target.transform.dirty = true;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };

        // channel.target.transform.set({
        //   position: vec3.copy(currentOutputVec3),
        // });
      } else if (channel.path === "scale") {
        vec3.set(channel.target.transform.scale, currentOutputVec3);
        channel.target.transform.dirty = true;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };
        // channel.target.transform.set({
        //   scale: vec3.copy(currentOutputVec3),
        // });
      } else if (channel.path === "weights") {
        // channel.target.getComponent("Morph").set({
        //   weights: outputData[nextIndex].slice(),
        // });
      }
    }
  }
}

export default function createAnimationSystem(opts) {
  const animationSystem = {};

  animationSystem.updateAnimations = (animationEntities, deltaTime) => {
    for (let entity of animationEntities) {
      updateAnimation(entity.animation, deltaTime);
    }
  };

  animationSystem.updateSkins = (skinEntities, transformSystem) => {
    for (let entity of skinEntities) {
      //entity.skin
      for (let i = 0; i < entity.skin.joints.length; i++) {
        const joint = entity.skin.joints[i];
        const m = entity.skin.jointMatrices[i];
        mat4.identity(m);
        const modelMatrix = joint._transform.modelMatrix;
        mat4.mult(m, modelMatrix);
        mat4.mult(m, entity.skin.inverseBindMatrices[i]);
        // console.log(m)
      }
    }
  };

  animationSystem.update = (entities, deltaTime) => {
    const animationEntities = entities.filter((e) => e.animation);
    animationSystem.updateAnimations(animationEntities, deltaTime);

    const skinEntities = entities.filter((e) => e.skin);
    animationSystem.updateSkins(skinEntities, deltaTime);
  };

  return animationSystem;
}
<commit_msg>fix(systems/animation): check for transform before accessing joint's model matrix<commit_after>import { vec3, vec4, mat4, quat, utils } from "pex-math";

let currentOutputVec3 = vec3.create();
let currentOutputQuat = quat.create();

function updateAnimation(animation, deltaTime) {
  // console.log('updateAnimation', animation)
  // if (!animation.enabled) return;

  if (!animation.prevTime) {
    animation.time = 0;
    animation.prevTime = Date.now();
    animation.playing = true;
  }

  if (animation.playing) {
    const animationLength =
      animation.duration ||
      animation.channels[0].input[animation.channels[0].input.length - 1];
    const now = Date.now();
    // const deltaTime = (now - animation.prevTime) / 1000;

    animation.prevTime = now;
    animation.time += deltaTime;
    if (animation.time > animationLength) {
      if (animation.loop) {
        animation.time %= animationLength;
      } else {
        animation.time = 0;
        animation.playing = false;
        // animation.set({ playing: false });
      }
    }

    animation.needsUpdate = true;
  }

  // console.log(animation)
  if (!animation.needsUpdate) return;

  animation.needsUpdate = false;

  for (let i = 0; i < animation.channels.length; i++) {
    const channel = animation.channels[i];
    const inputData = channel.input;

    let prevIndex;
    let nextIndex;

    for (let j = 0; j < inputData.length; j++) {
      nextIndex = j;
      if (inputData[j] >= animation.time) {
        break;
      }
      prevIndex = nextIndex;
    }

    const isRotation = channel.path === "rotation";
    const outputData = channel.output;
    const prevInput = inputData[prevIndex];
    const nextInput = inputData[nextIndex];
    const scale = nextInput - prevInput || 1;

    const t = (animation.time - prevInput) / scale;

    // console.log('t', t)

    if (prevIndex !== undefined) {
      switch (channel.interpolation) {
        case "STEP":
          if (isRotation) {
            quat.set(currentOutputQuat, outputData[prevIndex]);
          } else {
            vec3.set(currentOutputVec3, outputData[prevIndex]);
          }
          break;
        case "CUBICSPLINE": {
          const vec = isRotation ? vec4 : vec3;
          const tt = t * t;
          const ttt = tt * t;

          // Each input value corresponds to three output values of the same type: in-tangent, data point, and out-tangent.
          // p0
          const prevPosition = vec.copy(outputData[prevIndex * 3 + 1]);

          // p1
          const nextPos = vec.copy(outputData[nextIndex * 3 + 1]);

          // m0 = (tk+1 - tk)bk
          const prevOutTangent = prevIndex
            ? vec.scale(vec.copy(outputData[prevIndex * 3 + 2]), scale)
            : vec.create();

          // m1 = (tk+1 - tk)ak+1
          const nextInTangent =
            nextIndex !== inputData.length - 1
              ? vec.scale(vec.copy(outputData[prevIndex * 3]), scale)
              : vec.create();

          // p(t) = (2t³ - 3t² + 1)p0 + (t³ - 2t² + t)m0 + (-2t³ + 3t²)p1 + (t³ - t²)m1
          const p0 = vec.scale(prevPosition, 2 * ttt - 3 * tt + 1);
          const m0 = vec.scale(prevOutTangent, ttt - 2 * tt + t);
          const p1 = vec.scale(nextPos, -2 * ttt + 3 * tt);
          const m1 = vec.scale(nextInTangent, ttt - tt);

          if (isRotation) {
            quat.set(
              currentOutputQuat,
              quat.normalize([
                p0[0] + m0[0] + p1[0] + m1[0],
                p0[1] + m0[1] + p1[1] + m1[1],
                p0[2] + m0[2] + p1[2] + m1[2],
                p0[3] + m0[3] + p1[3] + m1[3],
              ])
            );
          } else {
            vec3.set(
              currentOutputVec3,
              vec3.add(vec3.add(vec3.add(p0, m0), p1), m1)
            );
          }

          break;
        }
        default:
          // LINEAR
          if (isRotation) {
            quat.slerp(
              quat.set(currentOutputQuat, outputData[prevIndex]),
              outputData[nextIndex],
              t
            );
            // console.log(
            //   "currentOutputQuat",
            //   // prevIndex,
            //   // outputData[prevIndex],
            //   // nextIndex,
            //   // outputData[nextIndex],
            //   t,
            //   currentOutputQuat
            // );
          } else {
            vec3.set(
              currentOutputVec3,
              outputData[nextIndex].map((output, index) =>
                utils.lerp(outputData[prevIndex][index], output, t)
              )
            );
          }
      }

      if (isRotation) {
        quat.set(channel.target.transform.rotation, currentOutputQuat);
        channel.target.transform.dirty = true;
        // console.log(
        //   "channel.target.transform.rotation",
        //   channel.target.transform.rotation
        // );
        window.transform = channel.target.transform;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };
        // channel.target.transform.set({
        //   rotation: quat.copy(currentOutputQuat),
        // });
      } else if (channel.path === "translation") {
        vec3.set(channel.target.transform.position, currentOutputVec3);
        channel.target.transform.dirty = true;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };

        // channel.target.transform.set({
        //   position: vec3.copy(currentOutputVec3),
        // });
      } else if (channel.path === "scale") {
        vec3.set(channel.target.transform.scale, currentOutputVec3);
        channel.target.transform.dirty = true;
        // channel.target.transform = {
        //   ...channel.target.transform,
        // };
        // channel.target.transform.set({
        //   scale: vec3.copy(currentOutputVec3),
        // });
      } else if (channel.path === "weights") {
        // channel.target.getComponent("Morph").set({
        //   weights: outputData[nextIndex].slice(),
        // });
      }
    }
  }
}

export default function createAnimationSystem(opts) {
  const animationSystem = {};

  animationSystem.updateAnimations = (animationEntities, deltaTime) => {
    for (let entity of animationEntities) {
      updateAnimation(entity.animation, deltaTime);
    }
  };

  animationSystem.updateSkins = (skinEntities, transformSystem) => {
    for (let entity of skinEntities) {
      //entity.skin
      for (let i = 0; i < entity.skin.joints.length; i++) {
        const joint = entity.skin.joints[i];
        const m = entity.skin.jointMatrices[i];
        mat4.identity(m);
        if (joint._transform) {
          const modelMatrix = joint._transform.modelMatrix;
          mat4.mult(m, modelMatrix);
          mat4.mult(m, entity.skin.inverseBindMatrices[i]);
        }
        // console.log(m)
      }
    }
  };

  animationSystem.update = (entities, deltaTime) => {
    const animationEntities = entities.filter((e) => e.animation);
    animationSystem.updateAnimations(animationEntities, deltaTime);

    const skinEntities = entities.filter((e) => e.skin);
    animationSystem.updateSkins(skinEntities, deltaTime);
  };

  return animationSystem;
}
<|endoftext|>


<commit_before>// Todo:
// 1) Make the button prettier
// 2) add a config option for IE users which takes a URL.  That URL should accept a POST request with a
//    JSON encoded object in the payload and return a CSV.  This is necessary because IE doesn't let you
//    download from a data-uri link
//
// Notes:  This has not been adequately tested and is very much a proof of concept at this point
function ngGridCsvExportPlugin (opts) {
    var self = this;
    self.grid = null;
    self.scope = null;
    self.init = function(scope, grid, services) {
        self.grid = grid;
        self.scope = scope;
        function showDs() {
            var keys = [];
            for (var f in grid.config.columnDefs) { keys.push(grid.config.columnDefs[f].field);}
            var csvData = '';
            function csvStringify(str) {
                if (str == null) { // we want to catch anything null-ish, hence just == not ===
                    return '';
                }
                if (typeof(str) === 'number') {
                    return '' + str;
                }
                if (typeof(str) === 'boolean') {
                    return (str ? 'TRUE' : 'FALSE') ;
                }
                if (typeof(str) === 'string') {
                    return str.replace(/"/g,'""');
                }

                return JSON.stringify(str).replace(/"/g,'""');
            }
            function swapLastCommaForNewline(str) {
                var newStr = str.substr(0,str.length - 1);
                return newStr + "\n";
            }
            for (var k in keys) {
                csvData += '"' + csvStringify(keys[k]) + '",';
            }
            csvData = swapLastCommaForNewline(csvData);
            var gridData = grid.data;
            for (var gridRow in gridData) {
                for ( k in keys) {
                    var curCellRaw;
                    if (opts != null && opts.columnOverrides != null && opts.columnOverrides[keys[k]] != null) {
                        curCellRaw = opts.columnOverrides[keys[k]](gridData[gridRow][keys[k]]);
                    }
                    else {
                        curCellRaw = gridData[gridRow][keys[k]];
                    }
                    csvData += '"' + csvStringify(curCellRaw) + '",';
                }
                csvData = swapLastCommaForNewline(csvData);
            }
            var fp = grid.$root.find(".ngFooterPanel");
            var csvDataLinkPrevious = grid.$root.find('.ngFooterPanel .csv-data-link-span');
            if (csvDataLinkPrevious != null) {csvDataLinkPrevious.remove() ; }
            var csvDataLinkHtml = "";
            csvDataLinkHtml += "
CSV Export
" ;
            fp.append(csvDataLinkHtml);
        }
        setTimeout(showDs, 0);
        scope.catHashKeys = function() {
            var hash = '';
            for (var idx in scope.renderedRows) {
                hash += scope.renderedRows[idx].$$hashKey;
            }
            return hash;
        };
        if (opts.customDataWatcher) {
            scope.$watch(opts.customDataWatcher, showDs);
        } else {
            scope.$watch(scope.catHashKeys, showDs);
        }
    };
}
<commit_msg>ng-grid-csv-export.js: only stringify properties<commit_after>// Todo:
// 1) Make the button prettier
// 2) add a config option for IE users which takes a URL.  That URL should accept a POST request with a
//    JSON encoded object in the payload and return a CSV.  This is necessary because IE doesn't let you
//    download from a data-uri link
//
// Notes:  This has not been adequately tested and is very much a proof of concept at this point
function ngGridCsvExportPlugin (opts) {
    var self = this;
    self.grid = null;
    self.scope = null;
    self.init = function(scope, grid, services) {
        self.grid = grid;
        self.scope = scope;
        function showDs() {
            var keys = [];
            for (var f in grid.config.columnDefs) {
                if (grid.config.columnDefs.hasOwnProperty(f))
                {
                    keys.push(grid.config.columnDefs[f].field);
                }
            }
            var csvData = '';
            function csvStringify(str) {
                if (str == null) { // we want to catch anything null-ish, hence just == not ===
                    return '';
                }
                if (typeof(str) === 'number') {
                    return '' + str;
                }
                if (typeof(str) === 'boolean') {
                    return (str ? 'TRUE' : 'FALSE') ;
                }
                if (typeof(str) === 'string') {
                    return str.replace(/"/g,'""');
                }

                return JSON.stringify(str).replace(/"/g,'""');
            }
            function swapLastCommaForNewline(str) {
                var newStr = str.substr(0,str.length - 1);
                return newStr + "\n";
            }
            for (var k in keys) {
                csvData += '"' + csvStringify(keys[k]) + '",';
            }
            csvData = swapLastCommaForNewline(csvData);
            var gridData = grid.data;
            for (var gridRow in gridData) {
                for ( k in keys) {
                    var curCellRaw;
                    if (opts != null && opts.columnOverrides != null && opts.columnOverrides[keys[k]] != null) {
                        curCellRaw = opts.columnOverrides[keys[k]](gridData[gridRow][keys[k]]);
                    }
                    else {
                        curCellRaw = gridData[gridRow][keys[k]];
                    }
                    csvData += '"' + csvStringify(curCellRaw) + '",';
                }
                csvData = swapLastCommaForNewline(csvData);
            }
            var fp = grid.$root.find(".ngFooterPanel");
            var csvDataLinkPrevious = grid.$root.find('.ngFooterPanel .csv-data-link-span');
            if (csvDataLinkPrevious != null) {csvDataLinkPrevious.remove() ; }
            var csvDataLinkHtml = "";
            csvDataLinkHtml += "
CSV Export
" ;
            fp.append(csvDataLinkHtml);
        }
        setTimeout(showDs, 0);
        scope.catHashKeys = function() {
            var hash = '';
            for (var idx in scope.renderedRows) {
                hash += scope.renderedRows[idx].$$hashKey;
            }
            return hash;
        };
        if (opts.customDataWatcher) {
            scope.$watch(opts.customDataWatcher, showDs);
        } else {
            scope.$watch(scope.catHashKeys, showDs);
        }
    };
}
<|endoftext|>


<commit_before>import StringBuffer from 'discourse/mixins/string-buffer';
import { iconHTML } from 'discourse/helpers/fa-icon';

// Helper class for rendering a button
export const Button = function(action, label, icon, opts) {
  this.action = action;
  this.label = label;

  if (typeof icon === "object") {
    this.opts = icon;
  } else {
    this.icon = icon;
  }
  this.opts = this.opts || opts || {};
};

function animateHeart($elem, start, end, complete) {
  $elem.stop()
       .css('textIndent', start)
       .animate({ textIndent: end }, {
          complete: complete,
          step(now) {
            $(this).css('transform','scale('+now+')');
          },
          duration: 150
        }, 'linear');
}

Button.prototype.render = function(buffer) {
  const opts = this.opts;

  const label = I18n.t(this.label);

  buffer.push("<button aria-label=\"" + label +"\" " + "title=\"" + label + "\"");
  if (opts.disabled) { buffer.push(" disabled"); }
  if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
  if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
  if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
  buffer.push(" data-action=\"" + this.action + "\">");
  if (this.icon) { buffer.push(iconHTML(this.icon)); }
  if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
  if (opts.innerHTML) { buffer.push(opts.innerHTML); }
  buffer.push("</button>");
};

let hiddenButtons;

const PostMenuView = Ember.Component.extend(StringBuffer, {
  tagName: 'section',
  classNames: ['post-menu-area', 'clearfix'],

  rerenderTriggers: [
    'post.deleted_at',
    'likeAction.count',
    'likeAction.users.length',
    'post.reply_count',
    'post.showRepliesBelow',
    'post.can_delete',
    'post.bookmarked',
    'post.shareUrl',
    'post.topic.deleted_at',
    'post.replies.length',
    'post.wiki',
    'post.post_type',
    'collapsed'],

  likeAction: function() {
    return this.get('post.actionByName.like');
  }.property('post.actionByName.like'),

  _collapsedByDefault: function() {
    this.set('collapsed', true);
  }.on('init'),

  renderString(buffer) {
    const post = this.get('post');

    buffer.push("");
    this.renderReplies(post, buffer);
    this.renderLikes(post, buffer);
    this.renderButtons(post, buffer);
    this.renderAdminPopup(post, buffer);
    buffer.push("");
  },

  // Delegate click actions
  click(e) {
    const $target = $(e.target),
        action = $target.data('action') || $target.parent().data('action');

    if (!action) return;
    const handler = this["click" + action.capitalize()];
    if (!handler) return;

    handler.call(this, this.get('post'));
  },

  // Replies Button
  renderReplies(post, buffer) {
    if (!post.get('showRepliesBelow')) return;

    const replyCount = post.get('reply_count');
    buffer.push("<button class='show-replies' data-action='replies'>");
    buffer.push("" + replyCount + "");
    buffer.push(I18n.t("post.has_replies", { count: replyCount }));

    const icon = (this.get('post.replies.length') > 0) ? 'chevron-up' : 'chevron-down';
    return buffer.push(iconHTML(icon) + "</button>");
  },

  renderLikes(post, buffer) {
    const likeCount = this.get('likeAction.count') || 0;
    if (likeCount === 0) { return; }

    buffer.push("<button class='show-likes' data-action='likes'>");
    buffer.push("" + likeCount + "");
    buffer.push(I18n.t("post.has_likes", { count: likeCount }));

    const icon = (this.get('likeAction.users.length') > 0) ? 'chevron-up' : 'chevron-down';
    return buffer.push(iconHTML(icon) + "</button>");
  },

  renderButtons(post, buffer) {
    const self = this;
    const allButtons = [];
    let visibleButtons = [];

    if (typeof hiddenButtons === "undefined") {
      if (!Em.isEmpty(this.siteSettings.post_menu_hidden_items)) {
        hiddenButtons = this.siteSettings.post_menu_hidden_items.split('|');
      } else {
        hiddenButtons = [];
      }
    }

    if (post.get("bookmarked")) {
      hiddenButtons.removeObject("bookmark");
    }

    const yours = post.get('yours');
    this.siteSettings.post_menu.split("|").forEach(function(i) {
      const creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
      if (creator) {
        const button = creator.call(self, post);
        if (button) {
          allButtons.push(button);
          if ((yours && button.opts.alwaysShowYours) ||
              (post.get('wiki') && button.opts.alwaysShowWiki) ||
              (hiddenButtons.indexOf(i) === -1)) {
            visibleButtons.push(button);
          }
        }
      }
    });

    // Only show ellipsis if there is more than one button hidden
    // if there are no more buttons, we are not collapsed
    const collapsed = this.get('collapsed');
    if (!collapsed || (allButtons.length <= visibleButtons.length + 1)) {
      visibleButtons = allButtons;
      if (collapsed) { this.set('collapsed', false); }
    } else {
      visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
    }

    const callbacks = PostMenuView._registerButtonCallbacks;
    if (callbacks) {
      _.each(callbacks, function(callback) {
        callback.apply(self, [visibleButtons]);
      });
    }

    buffer.push('
');
    visibleButtons.forEach((b) => b.render(buffer));
    buffer.push("
");
  },

  clickLikes() {
    const likeAction = this.get('post.actionByName.like');
    if (likeAction) {
      const users = likeAction.get('users');
      if (users && users.length) {
        users.clear();
      } else {
        likeAction.loadUsers(this.get('post'));
      }
    }
  },

  clickReplies() {
    if (this.get('post.replies.length') > 0) {
      this.set('post.replies', []);
    } else {
      this.get('post').loadReplies();
    }
  },

  // Delete button
  buttonForDelete(post) {
    let label, icon;

    if (post.get('post_number') === 1) {
      // If it's the first post, the delete/undo actions are related to the topic
      const topic = post.get('topic');
      if (topic.get('deleted_at')) {
        if (!topic.get('details.can_recover')) { return; }
        label = "topic.actions.recover";
        icon = "undo";
      } else {
        if (!topic.get('details.can_delete')) { return; }
        label = "topic.actions.delete";
        icon = "trash-o";
      }

    } else {
      // The delete actions target the post iteself
      if (post.get('deleted_at') || post.get('user_deleted')) {
        if (!post.get('can_recover')) { return; }
        label = "post.controls.undelete";
        icon = "undo";
      } else {
        if (!post.get('can_delete')) { return; }
        label = "post.controls.delete";
        icon = "trash-o";
      }
    }
    const action = (icon === 'trash-o') ? 'delete' : 'recover';
    let opts;
    if (icon === "trash-o"){
      opts = {className: 'delete'};
    }
    return new Button(action, label, icon, opts);
  },

  clickRecover(post) {
    this.sendAction('recoverPost', post);
  },

  clickDelete(post) {
    this.sendAction('deletePost', post);
  },

  // Like button
  buttonForLike(post) {
    const likeAction = this.get('likeAction');
    if (!likeAction) { return; }

    const className = likeAction.get('acted') ? 'has-like' : 'like';
    if (likeAction.get('canToggle')) {
      const descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
      return new Button('like', descKey, 'heart', {className: className});
    } else if (likeAction.get('acted')) {
      return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true});
    }
  },

  clickLike(post) {
    const $heart = this.$('.fa-heart'),
          $likeButton = this.$('button[data-action=like]'),
          acted = post.get('actionByName.like.acted'),
          self = this;

    if (acted) {
      this.sendAction('toggleLike', post);
      $likeButton.removeClass('has-like').addClass('like');
    } else {
      const scale = [1.0, 1.5];
      animateHeart($heart, scale[0], scale[1], function() {
        animateHeart($heart, scale[1], scale[0], function() {
          self.sendAction('toggleLike', post);
          $likeButton.removeClass('like').addClass('has-like');
        });
      });
    }
  },

  // Flag button
  buttonForFlag(post) {
    if (Em.isEmpty(post.get('flagsAvailable'))) return;
    return new Button('flag', 'post.controls.flag', 'flag');
  },

  clickFlag(post) {
    this.sendAction('showFlags', post);
  },

  // Edit button
  buttonForEdit(post) {
    if (!post.get('can_edit')) return;
    return new Button('edit', 'post.controls.edit', 'pencil', {
      alwaysShowYours: true,
      alwaysShowWiki: true
    });
  },

  clickEdit(post) {
    this.sendAction('editPost', post);
  },

  // Share button
  buttonForShare(post) {
    if (!Discourse.User.current()) return;
    const options = {
      shareUrl: post.get('shareUrl'),
      postNumber: post.get('post_number')
    };
    return new Button('share', 'post.controls.share', 'link', options);
  },

  // Reply button
  buttonForReply() {
    if (!this.get('canCreatePost')) return;
    const options = {className: 'create'};

    if(!Discourse.Mobile.mobileView) {
      options.textLabel = 'topic.reply.title';
    }

    return new Button('reply', 'post.controls.reply', 'reply', options);
  },

  clickReply(post) {
    this.sendAction('replyToPost', post);
  },

  // Bookmark button
  buttonForBookmark(post) {
    if (!Discourse.User.current()) return;

    let iconClass = 'read-icon',
        buttonClass = 'bookmark',
        tooltip = 'bookmarks.not_bookmarked';

    if (post.get('bookmarked')) {
      iconClass += ' bookmarked';
      buttonClass += ' bookmarked';
      tooltip = 'bookmarks.created';
    }

    return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "
"});
  },

  clickBookmark(post) {
    this.sendAction('toggleBookmark', post);
  },

  buttonForAdmin() {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }
    return new Button('admin', 'post.controls.admin', 'wrench');
  },

  renderAdminPopup(post, buffer) {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }

    const isWiki = post.get('wiki'),
          wikiIcon = iconHTML('pencil-square-o'),
          wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
          isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
          postTypeIcon = iconHTML('shield'),
          postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
          rebakePostIcon = iconHTML('cog'),
          rebakePostText = I18n.t('post.controls.rebake'),
          unhidePostIcon = iconHTML('eye'),
          unhidePostText = I18n.t('post.controls.unhide');

    const html = '
' +
                 '
' + I18n.t('admin_title') + '
' +
                 '
' +
                   '
' + wikiIcon + wikiText + '
' +
                   (Discourse.User.currentProp('staff') ? '
' + postTypeIcon + postTypeText + '
' : '') +
                   '
' + rebakePostIcon + rebakePostText + '
' +
                   (post.hidden ? '
' + unhidePostIcon + unhidePostText + '
' : '') +
                 '
' +
               '
';

    buffer.push(html);
  },

  clickAdmin() {
    const $postAdminMenu = this.$(".post-admin-menu");
    $postAdminMenu.show();
    $("html").on("mouseup.post-admin-menu", function() {
      $postAdminMenu.hide();
      $("html").off("mouseup.post-admin-menu");
    });
  },

  clickToggleWiki() {
    this.sendAction('toggleWiki', this.get('post'));
  },

  clickTogglePostType() {
    this.sendAction("togglePostType", this.get("post"));
  },

  clickRebakePost() {
    this.sendAction("rebakePost", this.get("post"));
  },

  clickUnhidePost() {
    this.sendAction("unhidePost", this.get("post"));
  },

  buttonForShowMoreActions() {
    return new Button('showMoreActions', 'show_more', 'ellipsis-h');
  },

  clickShowMoreActions() {
    this.set('collapsed', false);
  }

});

PostMenuView.reopenClass({
  registerButton(callback){
    this._registerButtonCallbacks = this._registerButtonCallbacks || [];
    this._registerButtonCallbacks.push(callback);
  }
});

export default PostMenuView;
<commit_msg>FIX: JShint<commit_after>import StringBuffer from 'discourse/mixins/string-buffer';
import { iconHTML } from 'discourse/helpers/fa-icon';

// Helper class for rendering a button
export const Button = function(action, label, icon, opts) {
  this.action = action;
  this.label = label;

  if (typeof icon === "object") {
    this.opts = icon;
  } else {
    this.icon = icon;
  }
  this.opts = this.opts || opts || {};
};

function animateHeart($elem, start, end, complete) {
  $elem.stop()
       .css('textIndent', start)
       .animate({ textIndent: end }, {
          complete: complete,
          step(now) {
            $(this).css('transform','scale('+now+')');
          },
          duration: 150
        }, 'linear');
}

Button.prototype.render = function(buffer) {
  const opts = this.opts;

  const label = I18n.t(this.label);

  buffer.push("<button aria-label=\"" + label +"\" " + "title=\"" + label + "\"");
  if (opts.disabled) { buffer.push(" disabled"); }
  if (opts.className) { buffer.push(" class=\"" + opts.className + "\""); }
  if (opts.shareUrl) { buffer.push(" data-share-url=\"" + opts.shareUrl + "\""); }
  if (opts.postNumber) { buffer.push(" data-post-number=\"" + opts.postNumber + "\""); }
  buffer.push(" data-action=\"" + this.action + "\">");
  if (this.icon) { buffer.push(iconHTML(this.icon)); }
  if (opts.textLabel) { buffer.push(I18n.t(opts.textLabel)); }
  if (opts.innerHTML) { buffer.push(opts.innerHTML); }
  buffer.push("</button>");
};

let hiddenButtons;

const PostMenuView = Ember.Component.extend(StringBuffer, {
  tagName: 'section',
  classNames: ['post-menu-area', 'clearfix'],

  rerenderTriggers: [
    'post.deleted_at',
    'likeAction.count',
    'likeAction.users.length',
    'post.reply_count',
    'post.showRepliesBelow',
    'post.can_delete',
    'post.bookmarked',
    'post.shareUrl',
    'post.topic.deleted_at',
    'post.replies.length',
    'post.wiki',
    'post.post_type',
    'collapsed'],

  likeAction: function() {
    return this.get('post.actionByName.like');
  }.property('post.actionByName.like'),

  _collapsedByDefault: function() {
    this.set('collapsed', true);
  }.on('init'),

  renderString(buffer) {
    const post = this.get('post');

    buffer.push("");
    this.renderReplies(post, buffer);
    this.renderLikes(post, buffer);
    this.renderButtons(post, buffer);
    this.renderAdminPopup(post, buffer);
    buffer.push("");
  },

  // Delegate click actions
  click(e) {
    const $target = $(e.target),
        action = $target.data('action') || $target.parent().data('action');

    if (!action) return;
    const handler = this["click" + action.capitalize()];
    if (!handler) return;

    handler.call(this, this.get('post'));
  },

  // Replies Button
  renderReplies(post, buffer) {
    if (!post.get('showRepliesBelow')) return;

    const replyCount = post.get('reply_count');
    buffer.push("<button class='show-replies' data-action='replies'>");
    buffer.push("" + replyCount + "");
    buffer.push(I18n.t("post.has_replies", { count: replyCount }));

    const icon = (this.get('post.replies.length') > 0) ? 'chevron-up' : 'chevron-down';
    return buffer.push(iconHTML(icon) + "</button>");
  },

  renderLikes(post, buffer) {
    const likeCount = this.get('likeAction.count') || 0;
    if (likeCount === 0) { return; }

    buffer.push("<button class='show-likes' data-action='likes'>");
    buffer.push("" + likeCount + "");
    buffer.push(I18n.t("post.has_likes", { count: likeCount }));

    const icon = (this.get('likeAction.users.length') > 0) ? 'chevron-up' : 'chevron-down';
    return buffer.push(iconHTML(icon) + "</button>");
  },

  renderButtons(post, buffer) {
    const self = this;
    const allButtons = [];
    let visibleButtons = [];

    if (typeof hiddenButtons === "undefined") {
      if (!Em.isEmpty(this.siteSettings.post_menu_hidden_items)) {
        hiddenButtons = this.siteSettings.post_menu_hidden_items.split('|');
      } else {
        hiddenButtons = [];
      }
    }

    if (post.get("bookmarked")) {
      hiddenButtons.removeObject("bookmark");
    }

    const yours = post.get('yours');
    this.siteSettings.post_menu.split("|").forEach(function(i) {
      const creator = self["buttonFor" + i.replace(/\+/, '').capitalize()];
      if (creator) {
        const button = creator.call(self, post);
        if (button) {
          allButtons.push(button);
          if ((yours && button.opts.alwaysShowYours) ||
              (post.get('wiki') && button.opts.alwaysShowWiki) ||
              (hiddenButtons.indexOf(i) === -1)) {
            visibleButtons.push(button);
          }
        }
      }
    });

    // Only show ellipsis if there is more than one button hidden
    // if there are no more buttons, we are not collapsed
    const collapsed = this.get('collapsed');
    if (!collapsed || (allButtons.length <= visibleButtons.length + 1)) {
      visibleButtons = allButtons;
      if (collapsed) { this.set('collapsed', false); }
    } else {
      visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
    }

    const callbacks = PostMenuView._registerButtonCallbacks;
    if (callbacks) {
      _.each(callbacks, function(callback) {
        callback.apply(self, [visibleButtons]);
      });
    }

    buffer.push('
');
    visibleButtons.forEach((b) => b.render(buffer));
    buffer.push("
");
  },

  clickLikes() {
    const likeAction = this.get('post.actionByName.like');
    if (likeAction) {
      const users = likeAction.get('users');
      if (users && users.length) {
        users.clear();
      } else {
        likeAction.loadUsers(this.get('post'));
      }
    }
  },

  clickReplies() {
    if (this.get('post.replies.length') > 0) {
      this.set('post.replies', []);
    } else {
      this.get('post').loadReplies();
    }
  },

  // Delete button
  buttonForDelete(post) {
    let label, icon;

    if (post.get('post_number') === 1) {
      // If it's the first post, the delete/undo actions are related to the topic
      const topic = post.get('topic');
      if (topic.get('deleted_at')) {
        if (!topic.get('details.can_recover')) { return; }
        label = "topic.actions.recover";
        icon = "undo";
      } else {
        if (!topic.get('details.can_delete')) { return; }
        label = "topic.actions.delete";
        icon = "trash-o";
      }

    } else {
      // The delete actions target the post iteself
      if (post.get('deleted_at') || post.get('user_deleted')) {
        if (!post.get('can_recover')) { return; }
        label = "post.controls.undelete";
        icon = "undo";
      } else {
        if (!post.get('can_delete')) { return; }
        label = "post.controls.delete";
        icon = "trash-o";
      }
    }
    const action = (icon === 'trash-o') ? 'delete' : 'recover';
    let opts;
    if (icon === "trash-o"){
      opts = {className: 'delete'};
    }
    return new Button(action, label, icon, opts);
  },

  clickRecover(post) {
    this.sendAction('recoverPost', post);
  },

  clickDelete(post) {
    this.sendAction('deletePost', post);
  },

  // Like button
  buttonForLike() {
    const likeAction = this.get('likeAction');
    if (!likeAction) { return; }

    const className = likeAction.get('acted') ? 'has-like' : 'like';
    if (likeAction.get('canToggle')) {
      const descKey = likeAction.get('acted') ? 'post.controls.undo_like' : 'post.controls.like';
      return new Button('like', descKey, 'heart', {className: className});
    } else if (likeAction.get('acted')) {
      return new Button('like', 'post.controls.has_liked', 'heart', {className: className, disabled: true});
    }
  },

  clickLike(post) {
    const $heart = this.$('.fa-heart'),
          $likeButton = this.$('button[data-action=like]'),
          acted = post.get('actionByName.like.acted'),
          self = this;

    if (acted) {
      this.sendAction('toggleLike', post);
      $likeButton.removeClass('has-like').addClass('like');
    } else {
      const scale = [1.0, 1.5];
      animateHeart($heart, scale[0], scale[1], function() {
        animateHeart($heart, scale[1], scale[0], function() {
          self.sendAction('toggleLike', post);
          $likeButton.removeClass('like').addClass('has-like');
        });
      });
    }
  },

  // Flag button
  buttonForFlag(post) {
    if (Em.isEmpty(post.get('flagsAvailable'))) return;
    return new Button('flag', 'post.controls.flag', 'flag');
  },

  clickFlag(post) {
    this.sendAction('showFlags', post);
  },

  // Edit button
  buttonForEdit(post) {
    if (!post.get('can_edit')) return;
    return new Button('edit', 'post.controls.edit', 'pencil', {
      alwaysShowYours: true,
      alwaysShowWiki: true
    });
  },

  clickEdit(post) {
    this.sendAction('editPost', post);
  },

  // Share button
  buttonForShare(post) {
    if (!Discourse.User.current()) return;
    const options = {
      shareUrl: post.get('shareUrl'),
      postNumber: post.get('post_number')
    };
    return new Button('share', 'post.controls.share', 'link', options);
  },

  // Reply button
  buttonForReply() {
    if (!this.get('canCreatePost')) return;
    const options = {className: 'create'};

    if(!Discourse.Mobile.mobileView) {
      options.textLabel = 'topic.reply.title';
    }

    return new Button('reply', 'post.controls.reply', 'reply', options);
  },

  clickReply(post) {
    this.sendAction('replyToPost', post);
  },

  // Bookmark button
  buttonForBookmark(post) {
    if (!Discourse.User.current()) return;

    let iconClass = 'read-icon',
        buttonClass = 'bookmark',
        tooltip = 'bookmarks.not_bookmarked';

    if (post.get('bookmarked')) {
      iconClass += ' bookmarked';
      buttonClass += ' bookmarked';
      tooltip = 'bookmarks.created';
    }

    return new Button('bookmark', tooltip, {className: buttonClass, innerHTML: "
"});
  },

  clickBookmark(post) {
    this.sendAction('toggleBookmark', post);
  },

  buttonForAdmin() {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }
    return new Button('admin', 'post.controls.admin', 'wrench');
  },

  renderAdminPopup(post, buffer) {
    if (!Discourse.User.currentProp('canManageTopic')) { return; }

    const isWiki = post.get('wiki'),
          wikiIcon = iconHTML('pencil-square-o'),
          wikiText = isWiki ? I18n.t('post.controls.unwiki') : I18n.t('post.controls.wiki'),
          isModerator = post.get('post_type') === this.site.get('post_types.moderator_action'),
          postTypeIcon = iconHTML('shield'),
          postTypeText = isModerator ? I18n.t('post.controls.revert_to_regular') : I18n.t('post.controls.convert_to_moderator'),
          rebakePostIcon = iconHTML('cog'),
          rebakePostText = I18n.t('post.controls.rebake'),
          unhidePostIcon = iconHTML('eye'),
          unhidePostText = I18n.t('post.controls.unhide');

    const html = '
' +
                 '
' + I18n.t('admin_title') + '
' +
                 '
' +
                   '
' + wikiIcon + wikiText + '
' +
                   (Discourse.User.currentProp('staff') ? '
' + postTypeIcon + postTypeText + '
' : '') +
                   '
' + rebakePostIcon + rebakePostText + '
' +
                   (post.hidden ? '
' + unhidePostIcon + unhidePostText + '
' : '') +
                 '
' +
               '
';

    buffer.push(html);
  },

  clickAdmin() {
    const $postAdminMenu = this.$(".post-admin-menu");
    $postAdminMenu.show();
    $("html").on("mouseup.post-admin-menu", function() {
      $postAdminMenu.hide();
      $("html").off("mouseup.post-admin-menu");
    });
  },

  clickToggleWiki() {
    this.sendAction('toggleWiki', this.get('post'));
  },

  clickTogglePostType() {
    this.sendAction("togglePostType", this.get("post"));
  },

  clickRebakePost() {
    this.sendAction("rebakePost", this.get("post"));
  },

  clickUnhidePost() {
    this.sendAction("unhidePost", this.get("post"));
  },

  buttonForShowMoreActions() {
    return new Button('showMoreActions', 'show_more', 'ellipsis-h');
  },

  clickShowMoreActions() {
    this.set('collapsed', false);
  }

});

PostMenuView.reopenClass({
  registerButton(callback){
    this._registerButtonCallbacks = this._registerButtonCallbacks || [];
    this._registerButtonCallbacks.push(callback);
  }
});

export default PostMenuView;
<|endoftext|>