mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 15:17:25 +01:00
Merge branch 'develop' of github.com:HabitRPG/habitrpg into rebalancing
This commit is contained in:
1695
common/script/content.coffee
Normal file
1695
common/script/content.coffee
Normal file
File diff suppressed because it is too large
Load Diff
32
common/script/i18n.coffee
Normal file
32
common/script/i18n.coffee
Normal file
@@ -0,0 +1,32 @@
|
||||
_ = require 'lodash'
|
||||
|
||||
module.exports =
|
||||
strings: null, # Strings for one single language
|
||||
translations: {} # Strings for multiple languages {en: strings, de: strings, ...}
|
||||
t: (stringName) -> # Other parameters allowed are vars (Object) and locale (String)
|
||||
vars = arguments[1]
|
||||
|
||||
if _.isString(arguments[1])
|
||||
vars = null
|
||||
locale = arguments[1]
|
||||
else if arguments[2]?
|
||||
vars = arguments[1]
|
||||
locale = arguments[2]
|
||||
|
||||
locale = 'en' if (!locale? or (!module.exports.strings and !module.exports.translations[locale]))
|
||||
string = if (!module.exports.strings) then module.exports.translations[locale][stringName] else module.exports.strings[stringName]
|
||||
|
||||
clonedVars = _.clone(vars) or {};
|
||||
clonedVars.locale = locale;
|
||||
|
||||
if string
|
||||
try
|
||||
_.template(string, (clonedVars))
|
||||
catch e
|
||||
'Error processing string. Please report to http://github.com/HabitRPG/habitrpg.'
|
||||
else
|
||||
stringNotFound = if (!module.exports.strings) then module.exports.translations[locale].stringNotFound else module.exports.strings.stringNotFound
|
||||
try
|
||||
_.template(stringNotFound, {string: stringName})
|
||||
catch e
|
||||
'Error processing string. Please report to http://github.com/HabitRPG/habitrpg.'
|
||||
1669
common/script/index.coffee
Normal file
1669
common/script/index.coffee
Normal file
File diff suppressed because it is too large
Load Diff
50
common/script/public/config.js
Normal file
50
common/script/public/config.js
Normal file
@@ -0,0 +1,50 @@
|
||||
'use strict';
|
||||
angular.module('habitrpg').config(['$httpProvider', function($httpProvider){
|
||||
$httpProvider.interceptors.push(['$q', '$rootScope', function($q, $rootScope){
|
||||
return {
|
||||
response: function(response) {
|
||||
return response;
|
||||
},
|
||||
responseError: function(response) {
|
||||
var mobileApp = !!window.env.appVersion;
|
||||
|
||||
// Offline
|
||||
if (response.status == 0 ||
|
||||
// don't know why we're getting 404 here, should be 0
|
||||
(response.status == 404 && _.isEmpty(response.data))) {
|
||||
|
||||
if (!mobileApp) // skip mobile, queue actions
|
||||
$rootScope.$broadcast('responseText', window.env.t('serverUnreach'));
|
||||
|
||||
// Needs refresh
|
||||
} else if (response.needRefresh) {
|
||||
if (!mobileApp) // skip mobile for now
|
||||
$rootScope.$broadcast('responseError', "The site has been updated and the page needs to refresh. The last action has not been recorded, please refresh and try again.");
|
||||
|
||||
} else if (response.data.code && response.data.code === 'ACCOUNT_SUSPENDED') {
|
||||
confirm(response.data.err);
|
||||
localStorage.clear();
|
||||
window.location.href = mobileApp ? '/app/login' : '/logout'; //location.reload()
|
||||
|
||||
// 400 range?
|
||||
} else if (response.status < 500) {
|
||||
$rootScope.$broadcast('responseText', response.data.err || response.data);
|
||||
// Need to reject the prompse so the error is handled correctly
|
||||
if (response.status === 401)
|
||||
return $q.reject(response);
|
||||
|
||||
// Error
|
||||
} else {
|
||||
var error = '<strong>Please reload</strong>, ' +
|
||||
'"'+window.env.t('error')+' '+(response.data.err || response.data || 'something went wrong')+'" ' +
|
||||
window.env.t('seeConsole');
|
||||
if (mobileApp) error = 'Error contacting the server. Please try again in a few minutes.';
|
||||
$rootScope.$broadcast('responseError', error);
|
||||
console.error(response);
|
||||
}
|
||||
|
||||
return $q.reject(response);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
164
common/script/public/directives.js
Normal file
164
common/script/public/directives.js
Normal file
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Markdown
|
||||
* See http://www.heikura.me/#!/angularjs-markdown-directive
|
||||
*/
|
||||
(function(){
|
||||
var md = function () {
|
||||
marked.setOptions({
|
||||
gfm:true,
|
||||
pedantic:false,
|
||||
sanitize:true
|
||||
// callback for code highlighter
|
||||
// Uncomment this (and htljs.tabReplace below) if we add in highlight.js (http://www.heikura.me/#!/angularjs-markdown-directive)
|
||||
// highlight:function (code, lang) {
|
||||
// if (lang != undefined)
|
||||
// return hljs.highlight(lang, code).value;
|
||||
//
|
||||
// return hljs.highlightAuto(code).value;
|
||||
// }
|
||||
});
|
||||
|
||||
emoji.img_path = 'common/img/emoji/unicode/';
|
||||
|
||||
var toHtml = function (markdown) {
|
||||
if (markdown == undefined)
|
||||
return '';
|
||||
|
||||
markdown = marked(markdown);
|
||||
markdown = emoji.replace_colons(markdown);
|
||||
markdown = emoji.replace_unified(markdown);
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
// [nickgordon20131123] this hacky override wraps images with a link to the image in a new window, and also adds some classes in case we want to style
|
||||
marked.InlineLexer.prototype.outputLink = function(cap, link) {
|
||||
var escape = function(html, encode) {
|
||||
return html
|
||||
.replace(!encode ? /&(?!#?\w+;)/g : /&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
if (cap[0].charAt(0) !== '!') {
|
||||
return '<a class="markdown-link" target="_blank" href="'
|
||||
+ escape(link.href)
|
||||
+ '"'
|
||||
+ (link.title
|
||||
? ' title="'
|
||||
+ escape(link.title)
|
||||
+ '"'
|
||||
: '')
|
||||
+ '>'
|
||||
+ this.output(cap[1])
|
||||
+ '</a>';
|
||||
} else {
|
||||
return '<a class="markdown-img-link" target="_blank" href="'
|
||||
+ escape(link.href)
|
||||
+ '"'
|
||||
+ (link.title
|
||||
? ' title="'
|
||||
+ escape(link.title)
|
||||
+ '"'
|
||||
: '')
|
||||
+ '><img class="markdown-img" src="'
|
||||
+ escape(link.href)
|
||||
+ '" alt="'
|
||||
+ escape(cap[1])
|
||||
+ '"'
|
||||
+ (link.title
|
||||
? ' title="'
|
||||
+ escape(link.title)
|
||||
+ '"'
|
||||
: '')
|
||||
+ '></a>';
|
||||
}
|
||||
}
|
||||
|
||||
//hljs.tabReplace = ' ';
|
||||
|
||||
return {
|
||||
toHtml:toHtml
|
||||
};
|
||||
}();
|
||||
|
||||
habitrpg.directive('markdown', ['$timeout','MOBILE_APP', function($timeout, MOBILE_APP) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element, attrs) {
|
||||
var removeWatch = !!scope.$eval(attrs.removeWatch);
|
||||
var useTimeout = !!scope.$eval(attrs.useTimeout);
|
||||
var timeoutTime = scope.$eval(attrs.timeoutTime) || 0;
|
||||
|
||||
var doRemoveWatch = scope.$watch(attrs.text, function(value, oldValue) {
|
||||
var replaceMarkdown = function(){
|
||||
|
||||
var markdown = value;
|
||||
var linktarget = attrs.target || '_self';
|
||||
var userName = scope.User.user.profile.name;
|
||||
var userHighlight = "@"+userName;
|
||||
var html = md.toHtml(markdown);
|
||||
|
||||
html = html.replace(userHighlight, "<u>@"+userName+"</u>");
|
||||
|
||||
html = html.replace(' href',' target="'+linktarget+'" href');
|
||||
element.html(html);
|
||||
|
||||
if(MOBILE_APP) {
|
||||
var elements = element.find("a");
|
||||
_.forEach(elements, function(link){
|
||||
if(link.href) {
|
||||
|
||||
link.onclick = function (e) {
|
||||
scope.externalLink(this.href);
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if(removeWatch)
|
||||
{
|
||||
doRemoveWatch();
|
||||
}
|
||||
};
|
||||
|
||||
if(useTimeout)
|
||||
{
|
||||
$timeout(replaceMarkdown, timeoutTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
replaceMarkdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
habitrpg.filter('markdown', function() {
|
||||
return function(input){
|
||||
var html = md.toHtml(input);
|
||||
|
||||
html = html.replace(' href',' target="_self" href');
|
||||
|
||||
return html;
|
||||
};
|
||||
});
|
||||
})()
|
||||
|
||||
habitrpg.directive('questRewards', ['$rootScope', function($rootScope){
|
||||
return {
|
||||
restrict: 'AE',
|
||||
templateUrl: 'partials/options.social.party.quest-rewards.html',
|
||||
link: function(scope, element, attrs){
|
||||
scope.header = attrs.header || 'Rewards';
|
||||
scope.quest = $rootScope.Content.quests[attrs.key];
|
||||
}
|
||||
}
|
||||
}]);
|
||||
251
common/script/public/userServices.js
Normal file
251
common/script/public/userServices.js
Normal file
@@ -0,0 +1,251 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.service('ApiUrl', ['API_URL', function(currentApiUrl){
|
||||
this.setApiUrl = function(newUrl){
|
||||
currentApiUrl = newUrl;
|
||||
};
|
||||
|
||||
this.get = function(){
|
||||
return currentApiUrl;
|
||||
};
|
||||
}])
|
||||
|
||||
/**
|
||||
* Services that persists and retrieves user from localStorage.
|
||||
*/
|
||||
.factory('User', ['$rootScope', '$http', '$location', '$window', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'MOBILE_APP', 'Notification', 'ApiUrl',
|
||||
function($rootScope, $http, $location, $window, STORAGE_USER_ID, STORAGE_SETTINGS_ID, MOBILE_APP, Notification, ApiUrl) {
|
||||
var authenticated = false;
|
||||
var defaultSettings = {
|
||||
auth: { apiId: '', apiToken: ''},
|
||||
sync: {
|
||||
queue: [], //here OT will be queued up, this is NOT call-back queue!
|
||||
sent: [] //here will be OT which have been sent, but we have not got reply from server yet.
|
||||
},
|
||||
fetching: false, // whether fetch() was called or no. this is to avoid race conditions
|
||||
online: false
|
||||
};
|
||||
var settings = {}; //habit mobile settings (like auth etc.) to be stored here
|
||||
var user = {}; // this is stored as a reference accessible to all controllers, that way updates propagate
|
||||
|
||||
//first we populate user with schema
|
||||
user.apiToken = user._id = ''; // we use id / apitoken to determine if registered
|
||||
|
||||
//than we try to load localStorage
|
||||
if (localStorage.getItem(STORAGE_USER_ID)) {
|
||||
_.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID)));
|
||||
}
|
||||
user._wrapped = false;
|
||||
|
||||
var syncQueue = function (cb) {
|
||||
if (!authenticated) {
|
||||
$window.alert("Not authenticated, can't sync, go to settings first.");
|
||||
return;
|
||||
}
|
||||
|
||||
var queue = settings.sync.queue;
|
||||
var sent = settings.sync.sent;
|
||||
if (queue.length === 0) {
|
||||
// Sync: Queue is empty
|
||||
return;
|
||||
}
|
||||
if (settings.fetching) {
|
||||
// Sync: Already fetching
|
||||
return;
|
||||
}
|
||||
if (settings.online!==true) {
|
||||
// Sync: Not online
|
||||
return;
|
||||
}
|
||||
|
||||
settings.fetching = true;
|
||||
// move all actions from queue array to sent array
|
||||
_.times(queue.length, function () {
|
||||
sent.push(queue.shift());
|
||||
});
|
||||
|
||||
// Save the current filters
|
||||
var current_filters = user.filters;
|
||||
|
||||
$http.post(ApiUrl.get() + '/api/v2/user/batch-update', sent, {params: {data:+new Date, _v:user._v, siteVersion: $window.env && $window.env.siteVersion}})
|
||||
.success(function (data, status, heacreatingders, config) {
|
||||
//make sure there are no pending actions to sync. If there are any it is not safe to apply model from server as we may overwrite user data.
|
||||
if (!queue.length) {
|
||||
//we can't do user=data as it will not update user references in all other angular controllers.
|
||||
|
||||
// the user has been modified from another application, sync up
|
||||
if(data && data.wasModified) {
|
||||
delete data.wasModified;
|
||||
$rootScope.$emit('userUpdated', user);
|
||||
}
|
||||
|
||||
// Update user
|
||||
_.extend(user, data);
|
||||
// Preserve filter selections between syncs
|
||||
_.extend(user.filters,current_filters);
|
||||
if (!user._wrapped){
|
||||
|
||||
// This wraps user with `ops`, which are functions shared both on client and mobile. When performed on client,
|
||||
// they update the user in the browser and then send the request to the server, where the same operation is
|
||||
// replicated. We need to wrap each op to provide a callback to send that operation
|
||||
$window.habitrpgShared.wrap(user);
|
||||
_.each(user.ops, function(op,k){
|
||||
user.ops[k] = function(req,cb){
|
||||
if (cb) return op(req,cb);
|
||||
op(req,function(err,response){
|
||||
if (err) {
|
||||
var message = err.code ? err.message : err;
|
||||
console.log(message);
|
||||
if (MOBILE_APP) Notification.push({type:'text',text:message});
|
||||
else Notification.text(message);
|
||||
// In the case of 200s, they're friendly alert messages like "You're pet has hatched!" - still send the op
|
||||
if ((err.code && err.code >= 400) || !err.code) return;
|
||||
}
|
||||
userServices.log({op:k, params: req.params, query:req.query, body:req.body});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Emit event when user is synced
|
||||
$rootScope.$emit('userSynced');
|
||||
}
|
||||
sent.length = 0;
|
||||
settings.fetching = false;
|
||||
save();
|
||||
if (cb) {
|
||||
cb(false)
|
||||
}
|
||||
|
||||
syncQueue(); // call syncQueue to check if anyone pushed more actions to the queue while we were talking to server.
|
||||
})
|
||||
.error(function (data, status, headers, config) {
|
||||
// (Notifications handled in app.js)
|
||||
|
||||
// If we're offline, queue up offline actions so we can send when we're back online
|
||||
if (status === 0) {
|
||||
//move sent actions back to queue
|
||||
_.times(sent.length, function () {
|
||||
queue.push(sent.shift())
|
||||
});
|
||||
settings.fetching = false;
|
||||
// In the case of errors, discard the corrupt queue
|
||||
} else {
|
||||
// Clear the queue. Better if we can hunt down the problem op, but this is the easiest solution
|
||||
settings.sync.queue = settings.sync.sent = [];
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
var save = function () {
|
||||
localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user));
|
||||
localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings));
|
||||
};
|
||||
var userServices = {
|
||||
user: user,
|
||||
set: function(updates) {
|
||||
user.ops.update({body:updates});
|
||||
},
|
||||
online: function (status) {
|
||||
if (status===true) {
|
||||
settings.online = true;
|
||||
syncQueue();
|
||||
} else {
|
||||
settings.online = false;
|
||||
};
|
||||
},
|
||||
|
||||
authenticate: function (uuid, token, cb) {
|
||||
if (!!uuid && !!token) {
|
||||
$http.defaults.headers.common['x-api-user'] = uuid;
|
||||
$http.defaults.headers.common['x-api-key'] = token;
|
||||
authenticated = true;
|
||||
settings.auth.apiId = uuid;
|
||||
settings.auth.apiToken = token;
|
||||
settings.online = true;
|
||||
if (user && user._v) user._v--; // shortcut to always fetch new updates on page reload
|
||||
userServices.log({}, function(){
|
||||
// If they don't have timezone, set it
|
||||
var offset = moment().zone(); // eg, 240 - this will be converted on server as -(offset/60)
|
||||
if (user.preferences.timezoneOffset !== offset)
|
||||
userServices.set({'preferences.timezoneOffset': offset});
|
||||
cb && cb();
|
||||
});
|
||||
} else {
|
||||
alert('Please enter your ID and Token in settings.')
|
||||
}
|
||||
},
|
||||
|
||||
authenticated: function(){
|
||||
return this.settings.auth.apiId !== "";
|
||||
},
|
||||
|
||||
log: function (action, cb) {
|
||||
//push by one buy one if an array passed in.
|
||||
if (_.isArray(action)) {
|
||||
action.forEach(function (a) {
|
||||
settings.sync.queue.push(a);
|
||||
});
|
||||
} else {
|
||||
settings.sync.queue.push(action);
|
||||
}
|
||||
|
||||
save();
|
||||
syncQueue(cb);
|
||||
},
|
||||
|
||||
sync: function(){
|
||||
user._v--;
|
||||
userServices.log({});
|
||||
},
|
||||
|
||||
save: save,
|
||||
|
||||
settings: settings
|
||||
};
|
||||
|
||||
|
||||
//load settings if we have them
|
||||
if (localStorage.getItem(STORAGE_SETTINGS_ID)) {
|
||||
//use extend here to make sure we keep object reference in other angular controllers
|
||||
_.extend(settings, JSON.parse(localStorage.getItem(STORAGE_SETTINGS_ID)));
|
||||
|
||||
//if settings were saved while fetch was in process reset the flag.
|
||||
settings.fetching = false;
|
||||
//create and load if not
|
||||
} else {
|
||||
localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(defaultSettings));
|
||||
_.extend(settings, defaultSettings);
|
||||
}
|
||||
|
||||
//If user does not have ApiID that forward him to settings.
|
||||
if (!settings.auth.apiId || !settings.auth.apiToken) {
|
||||
|
||||
if (MOBILE_APP) {
|
||||
$location.path("/login");
|
||||
} else {
|
||||
//var search = $location.search(); // FIXME this should be working, but it's returning an empty object when at a root url /?_id=...
|
||||
var search = $location.search($window.location.search.substring(1)).$$search; // so we use this fugly hack instead
|
||||
if (search.err) return alert(search.err);
|
||||
if (search._id && search.apiToken) {
|
||||
userServices.authenticate(search._id, search.apiToken, function(){
|
||||
$window.location.href='/';
|
||||
});
|
||||
} else {
|
||||
if ($window.location.pathname.indexOf('/static') !== 0){
|
||||
localStorage.clear();
|
||||
$window.location.href = '/logout';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
userServices.authenticate(settings.auth.apiId, settings.auth.apiToken)
|
||||
}
|
||||
|
||||
return userServices;
|
||||
}
|
||||
]);
|
||||
Reference in New Issue
Block a user