Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d969bd447 | ||
|
|
b3aeaf06ea | ||
|
|
85e3b0724a | ||
|
|
81a3ede86e | ||
|
|
d4864dda5f | ||
|
|
5ce1b6aff8 | ||
|
|
41731212ef |
34
README.md
34
README.md
@@ -37,7 +37,7 @@ changes on the fly. Let's look at a hypothetical example.
|
||||
Getting started
|
||||
---------------
|
||||
|
||||
Running DiceCloud locally, either to host it yourself away from an internet
|
||||
Running DiceCloud locally, either to run it locally, away from an internet
|
||||
connection, or to contribute to developing it further, is fairly
|
||||
straightforward and it should work on Linux, Windows, and Mac.
|
||||
|
||||
@@ -45,18 +45,29 @@ You'll need to have installed:
|
||||
|
||||
- [git](https://www.atlassian.com/git/tutorials/install-git)
|
||||
- [Meteor](https://www.meteor.com/install)
|
||||
- [Bower](https://bower.io/)
|
||||
|
||||
Then, it's just a matter of cloning this repository into a folder, installing the bower dependencies and running
|
||||
`meteor` in the app directory.
|
||||
Then, it's just a matter of cloning this repository into a folder, installing the dependencies and running
|
||||
`meteor` in the app directory:
|
||||
|
||||
`git clone https://github.com/ThaumRystra/DiceCloud dicecloud`
|
||||
`cd dicecloud`
|
||||
`cd app`
|
||||
`bower install`
|
||||
`meteor npm install`
|
||||
`meteor`
|
||||
|
||||
You should see this:
|
||||
If you edit the source code at this point, Meteor will rebuild the server with
|
||||
your changes.
|
||||
|
||||
If you want to simulate a production environment, run `meteor --production`
|
||||
|
||||
This will minimize all the files served to your browser, and load a lot faster,
|
||||
in exchange for not watching the source code for changes.
|
||||
|
||||
Note that this is not how you should deploy Meteor to your own web server, that
|
||||
is documented here: https://guide.meteor.com/deployment.html
|
||||
|
||||
After running `meteor` or `meteor --production`, you should see this, possibly
|
||||
mixed with other logged text:
|
||||
|
||||
```
|
||||
=> Started proxy.
|
||||
@@ -69,3 +80,14 @@ You should see this:
|
||||
Now, visiting http://localhost:3000/ should show you an empty instance of
|
||||
DiceCloud running.
|
||||
|
||||
To stop the process when you are done (or if it gets stuck) press `ctrl-c`
|
||||
|
||||
## Adding default documents
|
||||
|
||||
Navigate to `/dataSources/srd/srdimport.js`, and follow the steps under
|
||||
'First Setup', running the code in your browser's console, while logged in to
|
||||
your own instance of DiceCloud.
|
||||
|
||||
Do not run code in your browser console on the live version of DiceCloud hosted
|
||||
at dicecloud.com, as doing so could result in a large number of denied requests
|
||||
to the server, and may get your account permanently banned.
|
||||
|
||||
@@ -25,32 +25,32 @@ chuangbo:marked
|
||||
reywood:iron-router-ga
|
||||
meteor-base@1.4.0
|
||||
mobile-experience@1.0.5
|
||||
mongo@1.6.0
|
||||
mongo@1.6.2
|
||||
blaze-html-templates
|
||||
session@1.1.8
|
||||
session@1.2.0
|
||||
jquery@1.11.10
|
||||
tracker@1.2.0
|
||||
logging@1.1.20
|
||||
reload@1.2.0
|
||||
reload@1.3.0
|
||||
ejson@1.1.0
|
||||
spacebars
|
||||
check@1.3.1
|
||||
useraccounts:iron-routing
|
||||
wizonesolutions:canonical
|
||||
standard-minifier-js@2.4.0
|
||||
shell-server@0.4.0
|
||||
seba:minifiers-autoprefixer
|
||||
nikogosovd:multiple-uihooks
|
||||
templates:array
|
||||
ecmascript@0.12.0
|
||||
ecmascript@0.12.4
|
||||
es5-shim@4.8.0
|
||||
differential:vulcanize
|
||||
reactive-dict@1.2.1
|
||||
reactive-dict@1.3.0
|
||||
ongoworks:speakingurl
|
||||
service-configuration@1.0.11
|
||||
google-config-ui@1.0.1
|
||||
dynamic-import@0.5.0
|
||||
dynamic-import@0.5.1
|
||||
ddp-rate-limiter@1.0.7
|
||||
rate-limit@1.0.9
|
||||
iron:router
|
||||
littledata:synced-cron
|
||||
montiapm:agent
|
||||
zodern:standard-minifier-js
|
||||
|
||||
@@ -1 +1 @@
|
||||
METEOR@1.8
|
||||
METEOR@1.8.1
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
accounts-base@1.4.3
|
||||
accounts-base@1.4.4
|
||||
accounts-google@1.3.2
|
||||
accounts-oauth@1.1.16
|
||||
accounts-password@1.5.1
|
||||
accounts-ui@1.3.1
|
||||
accounts-ui-unstyled@1.4.1
|
||||
accounts-ui-unstyled@1.4.2
|
||||
aldeed:collection2@2.10.0
|
||||
aldeed:collection2-core@1.2.0
|
||||
aldeed:schema-deny@1.1.0
|
||||
aldeed:schema-index@1.1.1
|
||||
aldeed:simple-schema@1.5.4
|
||||
allow-deny@1.1.0
|
||||
autoupdate@1.5.0
|
||||
babel-compiler@7.2.0
|
||||
autoupdate@1.6.0
|
||||
babel-compiler@7.3.4
|
||||
babel-runtime@1.3.0
|
||||
base64@1.0.11
|
||||
binary-heap@1.0.11
|
||||
@@ -19,7 +19,7 @@ blaze@2.3.3
|
||||
blaze-html-templates@1.1.2
|
||||
blaze-tools@1.0.10
|
||||
boilerplate-generator@1.6.0
|
||||
caching-compiler@1.2.0
|
||||
caching-compiler@1.2.1
|
||||
caching-html-compiler@1.1.3
|
||||
callback-hook@1.1.0
|
||||
check@1.3.1
|
||||
@@ -31,12 +31,12 @@ ddp@1.4.0
|
||||
ddp-client@2.3.3
|
||||
ddp-common@1.4.0
|
||||
ddp-rate-limiter@1.0.7
|
||||
ddp-server@2.2.0
|
||||
ddp-server@2.3.0
|
||||
deps@1.0.12
|
||||
diff-sequence@1.1.0
|
||||
diff-sequence@1.1.1
|
||||
differential:vulcanize@3.0.0
|
||||
dynamic-import@0.5.0
|
||||
ecmascript@0.12.0
|
||||
dynamic-import@0.5.1
|
||||
ecmascript@0.12.7
|
||||
ecmascript-runtime@0.7.0
|
||||
ecmascript-runtime-client@0.8.0
|
||||
ecmascript-runtime-server@0.7.1
|
||||
@@ -44,14 +44,14 @@ ecwyne:mathjs@0.25.0
|
||||
ejson@1.1.0
|
||||
email@1.2.3
|
||||
es5-shim@4.8.0
|
||||
fetch@0.1.0
|
||||
fetch@0.1.1
|
||||
geojson-utils@1.0.10
|
||||
google-config-ui@1.0.1
|
||||
google-oauth@1.2.6
|
||||
hot-code-push@1.0.4
|
||||
html-tools@1.0.11
|
||||
htmljs@1.0.11
|
||||
http@1.4.1
|
||||
http@1.4.2
|
||||
id-map@1.1.0
|
||||
inter-process-messaging@0.1.0
|
||||
iron:controller@1.0.12
|
||||
@@ -64,6 +64,7 @@ iron:router@1.1.2
|
||||
iron:url@1.1.0
|
||||
jquery@1.11.11
|
||||
lai:collection-extensions@0.2.1_1
|
||||
lamhieu:meteorx@2.0.1
|
||||
launch-screen@1.1.1
|
||||
less@2.8.0
|
||||
littledata:synced-cron@1.5.1
|
||||
@@ -72,44 +73,45 @@ localstorage@1.2.0
|
||||
logging@1.1.20
|
||||
matb33:collection-hooks@0.8.4
|
||||
mdg:validation-error@0.5.1
|
||||
meteor@1.9.2
|
||||
meteor@1.9.3
|
||||
meteor-base@1.4.0
|
||||
meteorhacks:subs-manager@1.6.4
|
||||
minifier-css@1.4.0
|
||||
minifier-js@2.4.0
|
||||
minifier-css@1.4.2
|
||||
minimongo@1.4.5
|
||||
mobile-experience@1.0.5
|
||||
mobile-status-bar@1.0.14
|
||||
modern-browsers@0.1.2
|
||||
modern-browsers@0.1.4
|
||||
modules@0.13.0
|
||||
modules-runtime@0.10.2
|
||||
momentjs:moment@2.22.2
|
||||
mongo@1.6.0
|
||||
mongo-decimal@0.1.0
|
||||
modules-runtime@0.10.3
|
||||
momentjs:moment@2.24.0
|
||||
mongo@1.6.2
|
||||
mongo-decimal@0.1.1
|
||||
mongo-dev-server@1.1.0
|
||||
mongo-id@1.0.7
|
||||
mongo-livedata@1.0.12
|
||||
montiapm:agent@2.35.0
|
||||
nikogosovd:multiple-uihooks@0.1.8
|
||||
npm-bcrypt@0.9.3
|
||||
npm-mongo@3.1.1
|
||||
oauth@1.2.3
|
||||
npm-mongo@3.1.2
|
||||
oauth@1.2.8
|
||||
oauth2@1.2.1
|
||||
observe-sequence@1.0.16
|
||||
ongoworks:speakingurl@9.0.0
|
||||
ordered-dict@1.1.0
|
||||
percolate:migrations@0.9.8
|
||||
promise@0.11.1
|
||||
promise@0.11.2
|
||||
raix:eventemitter@0.1.3
|
||||
random@1.1.0
|
||||
rate-limit@1.0.9
|
||||
reactive-dict@1.2.1
|
||||
reactive-dict@1.3.0
|
||||
reactive-var@1.0.11
|
||||
reload@1.2.0
|
||||
reload@1.3.0
|
||||
retry@1.1.0
|
||||
reywood:iron-router-ga@0.7.1
|
||||
routepolicy@1.1.0
|
||||
seba:minifiers-autoprefixer@1.1.1
|
||||
seba:minifiers-autoprefixer@1.1.2
|
||||
service-configuration@1.0.11
|
||||
session@1.1.8
|
||||
session@1.2.0
|
||||
sha@1.0.9
|
||||
shell-server@0.4.0
|
||||
socket-stream-client@0.2.2
|
||||
@@ -119,7 +121,6 @@ spacebars-compiler@1.1.3
|
||||
splendido:accounts-emails-field@1.2.0
|
||||
splendido:accounts-meld@1.3.1
|
||||
srp@1.0.12
|
||||
standard-minifier-js@2.4.0
|
||||
templates:array@1.0.3
|
||||
templating@1.3.2
|
||||
templating-compiler@1.3.3
|
||||
@@ -132,8 +133,9 @@ url@1.2.0
|
||||
useraccounts:core@1.14.2
|
||||
useraccounts:iron-routing@1.14.2
|
||||
useraccounts:polymer@1.14.2
|
||||
webapp@1.7.0
|
||||
webapp@1.7.4
|
||||
webapp-hashing@1.0.9
|
||||
wizonesolutions:canonical@0.0.5
|
||||
zimme:collection-behaviours@1.1.3
|
||||
zimme:collection-softremovable@1.0.5
|
||||
zodern:minifier-js@3.0.0
|
||||
zodern:standard-minifier-js@3.0.0
|
||||
|
||||
@@ -10,19 +10,21 @@ Schemas.Library = new SimpleSchema({
|
||||
|
||||
Libraries.attachSchema(Schemas.Library);
|
||||
|
||||
Libraries.after.remove(function(userId, library) {
|
||||
LibraryItems.remove({library: library._id});
|
||||
LibrarySpells.remove({library: library._id});
|
||||
});
|
||||
if (Meteor.isServer){
|
||||
Libraries.after.remove(function(userId, library) {
|
||||
LibraryItems.remove({library: library._id});
|
||||
LibrarySpells.remove({library: library._id});
|
||||
});
|
||||
}
|
||||
|
||||
Meteor.methods({
|
||||
removeLibrary: function(libraryId) {
|
||||
unshareLibraryWithMe: function(libraryId) {
|
||||
let library = Libraries.findOne(libraryId);
|
||||
let userId = Meteor.userId();
|
||||
let userId = Meteor.userId();
|
||||
|
||||
if (!library) return;
|
||||
if (library.owner === userId){
|
||||
Libraries.remove(libraryId);
|
||||
throw new Meteor.error("Can't unshare, you own this")
|
||||
} else {
|
||||
if (_.contains(library.readers, userId)){
|
||||
Libraries.update(libraryId, {$pull: {"readers": userId}});
|
||||
|
||||
@@ -8,6 +8,8 @@ Router.plugin("ensureSignedIn", {
|
||||
only: [
|
||||
"profile",
|
||||
"characterList",
|
||||
"library",
|
||||
"libraries",
|
||||
]
|
||||
});
|
||||
|
||||
@@ -118,11 +120,28 @@ Router.map(function() {
|
||||
},
|
||||
});
|
||||
|
||||
this.route("library", {
|
||||
this.route("libraries", {
|
||||
path: "/library",
|
||||
waitOn: function(){
|
||||
return subsManager.subscribe("customLibraries");
|
||||
},
|
||||
onAfterAction: function() {
|
||||
document.title = appName + " - Libraries";
|
||||
},
|
||||
fastRender: true,
|
||||
});
|
||||
|
||||
this.route("library", {
|
||||
path: "/library/:_id",
|
||||
waitOn: function(){
|
||||
return [
|
||||
subsManager.subscribe("libraryItems", this.params._id),
|
||||
subsManager.subscribe("singleLibrary", this.params._id),
|
||||
];
|
||||
},
|
||||
data: function() {
|
||||
return Libraries.findOne(this.params._id);
|
||||
},
|
||||
onAfterAction: function() {
|
||||
document.title = appName + " - Library";
|
||||
},
|
||||
|
||||
7
app/client/globalHelpers/isTier5.js
Normal file
7
app/client/globalHelpers/isTier5.js
Normal file
@@ -0,0 +1,7 @@
|
||||
Template.registerHelper("isTier5", function(){
|
||||
let user = Meteor.user();
|
||||
if (!user) return false;
|
||||
patreon = user.patreon;
|
||||
if (!patreon) return false;
|
||||
return patreon.entitledCents >= 500 || patreon.entitledCentsOverride >= 500;
|
||||
});
|
||||
19
app/client/globalHelpers/patreonLoginUrl.js
Normal file
19
app/client/globalHelpers/patreonLoginUrl.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const CLIENT_ID = Meteor.settings &&
|
||||
Meteor.settings.public.patreon &&
|
||||
Meteor.settings.public.patreon.clientId;
|
||||
|
||||
Template.registerHelper("patreonLoginUrl", function() {
|
||||
if (!CLIENT_ID) return;
|
||||
return formatUrl({
|
||||
protocol: 'https',
|
||||
host: 'patreon.com',
|
||||
pathname: '/oauth2/authorize',
|
||||
query: {
|
||||
response_type: 'code',
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect',
|
||||
state: Meteor.userId(),
|
||||
scope: 'identity',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -157,7 +157,7 @@ Template.inventory.events({
|
||||
}
|
||||
// Make the library item into a regular item
|
||||
let item = _.omit(result, "libraryName", "library", "attacks", "effects");
|
||||
delete item.settings.category;
|
||||
if (item.settings && item.settings.category) delete item.settings.category;
|
||||
// Update the item to match library item
|
||||
Items.update(itemId, {$set: item});
|
||||
// Copy over attacks and effects
|
||||
|
||||
@@ -4,19 +4,30 @@
|
||||
<p>
|
||||
To get started, add a feature
|
||||
</p>
|
||||
<div class="layout vertical end">
|
||||
<paper-button class="skip-button" style="color: #d13b2e">Skip</paper-button>
|
||||
</div>
|
||||
</paper-step>
|
||||
<paper-step id="step1" label="Add an effect">
|
||||
<p>
|
||||
Add a racial effect to set your speed
|
||||
</p>
|
||||
<div class="layout vertical end">
|
||||
<paper-button class="skip-button" style="color: #d13b2e">Skip</paper-button>
|
||||
</div>
|
||||
</paper-step>
|
||||
<paper-step id="step2" label="See the effect in action">
|
||||
<p>
|
||||
View your speed stat
|
||||
</p>
|
||||
<div class="layout vertical end">
|
||||
<paper-button class="skip-button" style="color: #d13b2e">Skip</paper-button>
|
||||
</div>
|
||||
</paper-step>
|
||||
<paper-step id="step3" label="Finish">
|
||||
Done! If you get stuck, be sure to check out the <a href="/guide">guide</a>, or ask for help using the feedback form
|
||||
<p>
|
||||
Done! If you get stuck, be sure to check out the <a href="/guide">guide</a>, or ask for help using the feedback form
|
||||
</p>
|
||||
<div class="layout vertical end">
|
||||
<paper-button class="done-button" style="color: #d13b2e">Finish</paper-button>
|
||||
</div>
|
||||
|
||||
@@ -46,6 +46,10 @@ Template.newUserStepper.events({
|
||||
const stepper = instance.find("paper-stepper");
|
||||
stepper.continue();
|
||||
},
|
||||
"click .skip-button": function(event, instance){
|
||||
const stepper = instance.find("paper-stepper");
|
||||
stepper.continue();
|
||||
},
|
||||
});
|
||||
|
||||
Template.stats.events({
|
||||
|
||||
@@ -44,14 +44,12 @@
|
||||
Characters
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
{{#if isTier5}}
|
||||
<a href="/library" tabindex="-1">
|
||||
<paper-icon-item id="libary">
|
||||
<iron-icon icon="book" item-icon></iron-icon>
|
||||
Library (beta)
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
{{/if}}
|
||||
<a href="/library" tabindex="-1">
|
||||
<paper-icon-item id="libary">
|
||||
<iron-icon icon="book" item-icon></iron-icon>
|
||||
Libraries (beta)
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
<a href="/guide" tabindex="-1">
|
||||
<paper-icon-item id="guide">
|
||||
<iron-icon icon="social:school" item-icon></iron-icon>
|
||||
|
||||
@@ -17,13 +17,6 @@ Template.appDrawer.helpers({
|
||||
let post = PatreonPosts.findOne({}, {sort: {date: -1}});
|
||||
return (post && post.link) || 'https://www.patreon.com/dicecloud';
|
||||
},
|
||||
isTier5: function(){
|
||||
let user = Meteor.user();
|
||||
if (!user) return false;
|
||||
patreon = user.patreon;
|
||||
if (!patreon) return false;
|
||||
return patreon.entitledCents >= 500 || patreon.entitledCentsOverride >= 500;
|
||||
},
|
||||
patreonTier: function(){
|
||||
let user = Meteor.user();
|
||||
if (!user) return;
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<template name="itemLibrary">
|
||||
{{#each libraries}}
|
||||
<div class="paper-font-subhead library-header layout horizontal center" data-id={{_id}} style="height: 40px;">
|
||||
<iron-icon icon="chevron-right" class="{{#if isOpen _id}}open{{/if}}">
|
||||
</iron-icon>
|
||||
<div class="flex">{{name}}</div>
|
||||
{{#if isOpen _id}}
|
||||
<div class="relative">
|
||||
<paper-icon-button icon="create" class="editLibrary"></paper-icon-button>
|
||||
{{#simpleTooltip}}Edit Library{{/simpleTooltip}}
|
||||
</div>
|
||||
<div class="relative">
|
||||
<paper-icon-button icon="add" class="addItem"></paper-icon-button>
|
||||
{{#simpleTooltip}}Add Item{{/simpleTooltip}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<iron-collapse opened={{isOpen _id}}>
|
||||
{{#each libraryItems}}
|
||||
<paper-item class="short item-name" data-id={{_id}}>
|
||||
{{name}}
|
||||
</paper-item>
|
||||
{{/each}}
|
||||
{{#unless ready _id}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/unless}}
|
||||
</iron-collapse>
|
||||
{{/each}}
|
||||
</template>
|
||||
@@ -1,94 +0,0 @@
|
||||
const librarySubs = new SubsManager();
|
||||
|
||||
Template.itemLibrary.onCreated(function(){
|
||||
this.selectedTab = new ReactiveVar("0");
|
||||
this.librariesOpen = new ReactiveVar([]);
|
||||
this.readyDict = new ReactiveDict();
|
||||
this.autorun(() => {
|
||||
// Subscribe to all open libraries
|
||||
_.each(this.librariesOpen.get(), (libraryId) => {
|
||||
var handle = librarySubs.subscribe("libraryItems", libraryId);
|
||||
this.autorun(() => {
|
||||
this.readyDict.set(libraryId, handle.ready());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Template.itemLibrary.helpers({
|
||||
selectedTab(){
|
||||
return Template.instance().selectedTab.get();
|
||||
},
|
||||
libraries(){
|
||||
let userId = Meteor.userId();
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
],
|
||||
});
|
||||
},
|
||||
libraryItems(){
|
||||
return LibraryItems.find({
|
||||
library: this._id
|
||||
},{
|
||||
sort: {name: 1}
|
||||
});
|
||||
},
|
||||
ready(libraryId){
|
||||
return Template.instance().readyDict.get(libraryId);
|
||||
},
|
||||
isOpen(libraryId){
|
||||
const librariesOpen = Template.instance().librariesOpen.get();
|
||||
return _.contains(librariesOpen, libraryId);
|
||||
},
|
||||
});
|
||||
|
||||
Template.itemLibrary.events({
|
||||
"click .library-header": function(event, template){
|
||||
let libs = template.librariesOpen.get();
|
||||
const libraryId = this._id;
|
||||
// Toggle whether this key is in the array or not
|
||||
if (_.contains(libs, libraryId)){
|
||||
libs = _.without(libs, libraryId);
|
||||
} else {
|
||||
libs.push(libraryId);
|
||||
}
|
||||
template.librariesOpen.set(libs);
|
||||
},
|
||||
"click .editLibrary": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryDialog",
|
||||
data: {libraryId},
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
returnElement: () => instance.find(`.library-header[data-id='${libraryId}']`),
|
||||
});
|
||||
},
|
||||
"click .addItem": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
var itemId = LibraryItems.insert({
|
||||
name: "New Library Item",
|
||||
library: libraryId,
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item-name[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
"click .item-name": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var itemId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item-name[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
})
|
||||
1
app/client/views/library/libraries.css
Normal file
1
app/client/views/library/libraries.css
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
35
app/client/views/library/libraries.html
Normal file
35
app/client/views/library/libraries.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<template name="libraries">
|
||||
<div class="fit layout vertical library">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar class="medium-tall app-grey white-text">
|
||||
<div top-item class="layout horizontal center" style="min-height: 56px;">
|
||||
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
|
||||
<div class="flex layout horizontal center" style="height: 40px; margin-left: 8px;">
|
||||
Libraries
|
||||
</div>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
{{#if isTier5}}
|
||||
<div class="flex layout vertical center" style="position: relative; padding: 0 16px;">
|
||||
<paper-material class="card" style="padding: 32px; max-width: 800px; width: 100%;">
|
||||
{{#each library in libraries}}
|
||||
<a href="/library/{{library._id}}" tabindex="-1">
|
||||
<paper-item class="library" data-id="{{library._id}}">
|
||||
<paper-item-body>
|
||||
<div>{{library.name}}</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
</a>
|
||||
{{/each}}
|
||||
</paper-material>
|
||||
</div>
|
||||
<div class="floatyButton">
|
||||
<paper-fab id="addLibrary" icon="add"></paper-fab>
|
||||
{{#simpleTooltip}}Add Library{{/simpleTooltip}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{> patronsOnly }}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
38
app/client/views/library/libraries.js
Normal file
38
app/client/views/library/libraries.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const librarySubs = new SubsManager();
|
||||
|
||||
Template.libraries.helpers({
|
||||
libraries(){
|
||||
let userId = Meteor.userId();
|
||||
let subs = Meteor.user() && Meteor.user().profile.librarySubscriptions;
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
{_id: {$in: subs}}
|
||||
],
|
||||
}, {
|
||||
sort: {name: 1},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Template.libraries.events({
|
||||
"click #addLibrary": function(event, instance){
|
||||
var libraryId = Libraries.insert({
|
||||
name: "New Library",
|
||||
owner: Meteor.userId(),
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "libraryDialog",
|
||||
data: {libraryId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.library[data-id='${libraryId}']`),
|
||||
callback(data){
|
||||
if (data && data.delete){
|
||||
Libraries.remove(libraryId);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
@@ -1,19 +0,0 @@
|
||||
.library .item-name {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library .library-header {
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.library .library-header iron-icon {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.library .library-header iron-icon.open {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@@ -2,31 +2,55 @@
|
||||
<div class="fit layout vertical library">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar class="medium-tall app-grey white-text">
|
||||
<div top-item class="layout horizontal center">
|
||||
<div top-item class="layout horizontal center" style="min-height: 56px;">
|
||||
<paper-icon-button icon="menu" drawer-toggle></paper-icon-button>
|
||||
<a href="/library"><paper-icon-button icon="arrow-back"></paper-icon-button></a>
|
||||
<div class="flex layout horizontal center" style="height: 40px; margin-left: 8px;">
|
||||
Item Library
|
||||
{{name}}
|
||||
</div>
|
||||
{{#if isTier5}}{{#if canUserEdit}}
|
||||
<paper-icon-button icon="settings" id="edit"></paper-icon-button>
|
||||
{{/if}}{{/if}}
|
||||
</div>
|
||||
<!--
|
||||
<div bottom-item>
|
||||
<paper-tabs id="libraryTabs" selected={{selectedTab}} class="app-grey white-text">
|
||||
<paper-tab name="items">Items</paper-tab>
|
||||
<paper-tab name="spells">Spells</paper-tab>
|
||||
</paper-tabs>
|
||||
</div>
|
||||
-->
|
||||
{{#if isTier5}}
|
||||
<div bottom-item class="layout horizontal center">
|
||||
<paper-input label="Search" class="search-input">
|
||||
<iron-icon icon="search" prefix></iron-icon>
|
||||
</paper-input>
|
||||
<div class="flex"></div>
|
||||
{{#if canUserSubscribe}}
|
||||
<paper-button style="color: rgba(255,255,255,0.87);" id="subscribe">
|
||||
<iron-icon icon="add-circle"></iron-icon>
|
||||
Subscribe
|
||||
</paper-button>
|
||||
{{else if canUserUnsubscribe}}
|
||||
<paper-button style="color: rgba(255,255,255,0.87);" id="unsubscribe">
|
||||
<iron-icon icon="remove-circle"></iron-icon>
|
||||
Unsubscribe
|
||||
</paper-button>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="flex" style="position: relative;">
|
||||
<!-- <iron-pages id="tabPages" class="fit" selected={{selectedTab}}> -->
|
||||
<div name="items" class="tab-page fit">{{> itemLibrary}}</div>
|
||||
<!-- <div name="spells" class="tab-page fit">{{! {{> spellLibrary}} }}</div>
|
||||
</iron-pages> -->
|
||||
</div>
|
||||
<div class="floatyButton">
|
||||
<paper-fab id="addLibrary" icon="add"></paper-fab>
|
||||
{{#simpleTooltip}}Add Library{{/simpleTooltip}}
|
||||
</div>
|
||||
{{#if isTier5}}
|
||||
<div class="flex layout vertical center" style="position: relative; padding: 0 16px;">
|
||||
<paper-material class="card" style="padding: 32px; max-width: 800px; width: 100%;">
|
||||
{{#each items}}
|
||||
<paper-item data-id={{_id}} class="item">
|
||||
<paper-item-body>
|
||||
<div>{{displayName}}</div>
|
||||
</paper-item-body>
|
||||
</paper-item>
|
||||
{{/each}}
|
||||
</paper-material>
|
||||
</div>
|
||||
<div class="floatyButton">
|
||||
<paper-fab id="addLibraryItem" icon="add"></paper-fab>
|
||||
{{#simpleTooltip}}Add Library Item{{/simpleTooltip}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{> patronsOnly }}
|
||||
{{/if}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,29 +1,120 @@
|
||||
const librarySubs = new SubsManager();
|
||||
|
||||
Template.library.onCreated(function(){
|
||||
this.selectedTab = new ReactiveVar("0");
|
||||
this.searchTerm = new ReactiveVar("");
|
||||
});
|
||||
|
||||
Template.library.helpers({
|
||||
selectedTab(){
|
||||
return Template.instance().selectedTab.get();
|
||||
items(){
|
||||
let search = Template.instance().searchTerm.get();
|
||||
if (search){
|
||||
return LibraryItems.find(
|
||||
{
|
||||
library: this._id,
|
||||
$or: [
|
||||
{
|
||||
name: {$regex: new RegExp(".*" + search + ".*", "gi")}
|
||||
},
|
||||
{
|
||||
libraryname: {$regex: new RegExp(".*" + search + ".*", "gi")}
|
||||
},
|
||||
],
|
||||
},
|
||||
{sort: {name: 1}},
|
||||
);
|
||||
} else {
|
||||
return LibraryItems.find(
|
||||
{library: this._id},
|
||||
{sort: {name: 1}},
|
||||
);
|
||||
}
|
||||
},
|
||||
displayName(){
|
||||
return this.libraryName || this.name;
|
||||
},
|
||||
canUserSubscribe(){
|
||||
let user = Meteor.user();
|
||||
let userId = user._id;
|
||||
return !(
|
||||
_.contains(this.readers, userId) ||
|
||||
_.contains(this.writers, userId) ||
|
||||
this.owner === userId ||
|
||||
_.contains(user.profile.librarySubscriptions, this._id)
|
||||
);
|
||||
},
|
||||
canUserUnsubscribe(){
|
||||
let user = Meteor.user();
|
||||
let userId = user._id;
|
||||
return (
|
||||
_.contains(user.profile.librarySubscriptions, this._id) ||
|
||||
_.contains(this.readers, userId)
|
||||
);
|
||||
},
|
||||
canUserEdit(){
|
||||
let userId = Meteor.userId();
|
||||
return (
|
||||
_.contains(this.writers, userId) ||
|
||||
this.owner === userId
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Template.library.events({
|
||||
"iron-select #libraryTabs": function(event, instance){
|
||||
instance.selectedTab.set(event.target.selected);
|
||||
"input .search-input, change .search-input": function(event, template){
|
||||
const value = event.currentTarget.value;
|
||||
template.searchTerm.set(value);
|
||||
},
|
||||
"click #addLibrary": function(event, instance){
|
||||
var libraryId = Libraries.insert({
|
||||
name: "New Library",
|
||||
owner: Meteor.userId(),
|
||||
});
|
||||
"click #edit": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryDialog",
|
||||
data: {libraryId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.library-header[data-id='${libraryId}']`),
|
||||
element: event.currentTarget.parentElement.parentElement,
|
||||
callback(data){
|
||||
if (data && data.delete){
|
||||
Router.go('/library');
|
||||
Tracker.afterFlush(function(){
|
||||
Libraries.remove(libraryId);
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
})
|
||||
"click #addLibraryItem": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var libraryId = this._id;
|
||||
var itemId = LibraryItems.insert({
|
||||
name: "New Library Item",
|
||||
library: libraryId,
|
||||
});
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
"click .item": function(event, instance){
|
||||
event.stopPropagation();
|
||||
var itemId = this._id;
|
||||
pushDialogStack({
|
||||
template: "libraryItemDialog",
|
||||
data: {itemId},
|
||||
element: event.currentTarget,
|
||||
returnElement: () => instance.find(`.item[data-id='${itemId}']`),
|
||||
});
|
||||
},
|
||||
"click #subscribe": function(event, instance){
|
||||
Meteor.users.update(Meteor.userId(), {
|
||||
$addToSet: {"profile.librarySubscriptions": this._id},
|
||||
});
|
||||
},
|
||||
"click #unsubscribe": function(event, instance){
|
||||
let userId = Meteor.userId();
|
||||
Meteor.users.update(userId, {
|
||||
$pull: {"profile.librarySubscriptions": this._id},
|
||||
});
|
||||
Meteor.call("unshareLibraryWithMe", this._id);
|
||||
},
|
||||
});
|
||||
|
||||
18
app/client/views/library/libraryDeleteConfirmation.html
Normal file
18
app/client/views/library/libraryDeleteConfirmation.html
Normal file
@@ -0,0 +1,18 @@
|
||||
<template name="libraryDeleteConfirmation">
|
||||
<div class="fit layout vertical">
|
||||
<app-header-layout has-scrolling-region class="feedback flex">
|
||||
<app-header fixed effects="waterfall">
|
||||
<app-toolbar>
|
||||
<div main-title>Delete Library</div>
|
||||
</app-toolbar>
|
||||
</app-header>
|
||||
<div class="form flex">
|
||||
Deleting a library cannot be undone<br>
|
||||
<paper-button id="deleteButton" raised>Delete Library and All Contents</paper-button>
|
||||
</div>
|
||||
</app-header-layout>
|
||||
<div class="buttons layout horizontal end-justified">
|
||||
<paper-button class="cancelButton"> Cancel </paper-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
8
app/client/views/library/libraryDeleteConfirmation.js
Normal file
8
app/client/views/library/libraryDeleteConfirmation.js
Normal file
@@ -0,0 +1,8 @@
|
||||
Template.libraryDeleteConfirmation.events({
|
||||
"click #deleteButton": function(event, instance) {
|
||||
popDialogStack(true);
|
||||
},
|
||||
"click .cancelButton": function(event, instance){
|
||||
popDialogStack();
|
||||
},
|
||||
});
|
||||
@@ -1,15 +1,39 @@
|
||||
<template name="libraryDialog">
|
||||
<div class="fit base-dialog layout vertical">
|
||||
<app-toolbar>
|
||||
<div main-title>{{library.name}}</div>
|
||||
<paper-icon-button id="deleteButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
icon="delete">
|
||||
<paper-icon-button id="backButton"
|
||||
icon="arrow-back">
|
||||
</paper-icon-button>
|
||||
<div main-title>{{library.name}}</div>
|
||||
<paper-menu-button class="character-menu" horizontal-align="right">
|
||||
<paper-icon-button icon="delete" class="dropdown-trigger" disabled="{{notOwner}}">
|
||||
</paper-icon-button>
|
||||
<paper-menu class="dropdown-content black87">
|
||||
<paper-button id="deleteButton" disabled="{{notOwner}}" raised>
|
||||
Delete library and all its contents
|
||||
</paper-button>
|
||||
</paper-menu>
|
||||
</paper-menu-button>
|
||||
</app-toolbar>
|
||||
<div class="form flex scroll-y" style="position: relative;">
|
||||
<paper-input id="libraryNameInput" class="fullwidth" label="Name" value={{library.name}}></paper-input>
|
||||
<hr style="margin: 24px 0; opacity: 0.4;">
|
||||
<paper-dropdown-menu label="Who can view and subscribe to this library">
|
||||
<dicecloud-selector class="visibilityDropdown dropdown-content" selected={{viewPermission}}>
|
||||
<paper-item name="whitelist">Only people I share with</paper-item>
|
||||
<paper-item name="public">Anyone with link</paper-item>
|
||||
</dicecloud-selector>
|
||||
</paper-dropdown-menu>
|
||||
{{#if library.public}}
|
||||
<div style="margin-top: 16px;">
|
||||
Share this link for others to subscribe to this library:
|
||||
</div>
|
||||
<a href="{{pathFor route='library' data=library}}" style="color: #d13b2e; font-size: 18px">
|
||||
{{urlFor route='library' data=library}}
|
||||
</a>
|
||||
{{/if}}
|
||||
<hr style="margin: 24px 0; opacity: 0.4;">
|
||||
<div class="paper-font-title" style="margin-top: 32px;">Share Directly</div>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input class="flex" id="userNameOrEmailInput" label="Share with username or email" floatinglabel></paper-input>
|
||||
<paper-button id="shareButton"
|
||||
|
||||
@@ -13,6 +13,10 @@ Template.libraryDialog.helpers({
|
||||
library(){
|
||||
return Libraries.findOne(this.libraryId);
|
||||
},
|
||||
viewPermission(){
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {public: 1}});
|
||||
return library && library.public ? "public" : "whitelist";
|
||||
},
|
||||
readers: function(){
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {readers: 1}});
|
||||
return library && library.readers;
|
||||
@@ -33,9 +37,17 @@ Template.libraryDialog.helpers({
|
||||
return "User not found";
|
||||
}
|
||||
},
|
||||
notOwner: function(){
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {owner: 1}});
|
||||
if (!library) return;
|
||||
return Meteor.userId() !== library.owner;
|
||||
},
|
||||
});
|
||||
|
||||
Template.libraryDialog.events({
|
||||
"click #backButton": function(){
|
||||
popDialogStack();
|
||||
},
|
||||
"input #libraryNameInput": _.debounce(function(event){
|
||||
const input = event.currentTarget;
|
||||
var name = input.value;
|
||||
@@ -53,8 +65,10 @@ Template.libraryDialog.events({
|
||||
}
|
||||
}, 300),
|
||||
"click #deleteButton": function(){
|
||||
Meteor.call("removeLibrary", this.libraryId);
|
||||
popDialogStack();
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {owner: 1}});
|
||||
if (Meteor.userId() === library.owner){
|
||||
popDialogStack({delete: true});
|
||||
}
|
||||
},
|
||||
"input #userNameOrEmailInput":
|
||||
function(event, instance){
|
||||
@@ -64,11 +78,25 @@ Template.libraryDialog.events({
|
||||
if (err){
|
||||
console.error(err);
|
||||
} else {
|
||||
console.log(result);
|
||||
instance.userId.set(result);
|
||||
}
|
||||
});
|
||||
},
|
||||
"iron-select .visibilityDropdown": function(event){
|
||||
var detail = event.originalEvent.detail;
|
||||
var value = detail.item.getAttribute("name");
|
||||
let public;
|
||||
if (value === "whitelist"){
|
||||
public = false;
|
||||
} else if (value === "public") {
|
||||
public = true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
var library = Libraries.findOne(this.libraryId, {fields: {public: 1}});
|
||||
if (library.public === public) return;
|
||||
Libraries.update(this.libraryId, {$set: {public}});
|
||||
},
|
||||
"click #shareButton": function(event, instance){
|
||||
var self = this;
|
||||
var permission = instance.find("#accessLevelMenu").selected;
|
||||
|
||||
@@ -8,32 +8,34 @@
|
||||
<paper-icon-button id="deleteButton"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
icon="delete">
|
||||
icon="delete"
|
||||
disabled="{{cantEdit}}">
|
||||
</paper-icon-button>
|
||||
</app-toolbar>
|
||||
<div class="form flex scroll-y" style="position: relative;">
|
||||
<paper-input id="libraryItemLibraryNameInput" class="fullwidth" label="Library name (optional)" value={{item.libraryName}}></paper-input>
|
||||
<paper-input id="libraryItemNameInput" class="fullwidth" label="Item name" value={{item.name}}></paper-input>
|
||||
{{#if ready}}
|
||||
<paper-input id="libraryItemLibraryNameInput" class="fullwidth" label="Library name (optional)" value={{item.libraryName}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input id="libraryItemNameInput" class="fullwidth" label="Item name" value={{item.name}} disabled="{{cantEdit}}"></paper-input>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input id="libraryItemPluralInput" class="flex" label="Plural name" value={{item.plural}}></paper-input>
|
||||
<paper-input id="libraryItemQuantityInput" class="flex" label="Quantity" type="number" value={{item.quantity}}></paper-input>
|
||||
<paper-checkbox id="incrementCheckbox" class="flex" checked={{item.settings.showIncrement}}>
|
||||
<paper-input id="libraryItemPluralInput" class="flex" label="Plural name" value={{item.plural}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input id="libraryItemQuantityInput" class="flex" label="Quantity" type="number" value={{item.quantity}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-checkbox id="incrementCheckbox" class="flex" checked={{item.settings.showIncrement}} disabled="{{cantEdit}}">
|
||||
Show Increment
|
||||
</paper-checkbox>
|
||||
</div>
|
||||
<div class="layout horizontal center wrap">
|
||||
<paper-input id="libraryItemValueInput" class="flex" label="Value" type="number" value={{item.value}}></paper-input>
|
||||
<paper-input id="libraryItemWeightInput" class="flex" label="Weight" type="number" value={{item.weight}}></paper-input>
|
||||
<paper-checkbox id="attunementCheckbox" class="flex" checked={{item.requiresAttunement}}>
|
||||
<paper-input id="libraryItemValueInput" class="flex" label="Value" type="number" value={{item.value}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input id="libraryItemWeightInput" class="flex" label="Weight" type="number" value={{item.weight}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-checkbox id="attunementCheckbox" class="flex" checked={{item.requiresAttunement}} disabled="{{cantEdit}}">
|
||||
Requires Attunement
|
||||
</paper-checkbox>
|
||||
</div>
|
||||
<paper-textarea id="libraryItemDescriptionInput" label="Description" value={{item.description}}></paper-textarea>
|
||||
<paper-textarea id="libraryItemDescriptionInput" label="Description" value={{item.description}} disabled="{{cantEdit}}"></paper-textarea>
|
||||
<div style="margin-top: 8px;">
|
||||
<div class="paper-font-subhead">Effects</div>
|
||||
{{#each indexedEffects}}
|
||||
<div class="effect layout horizontal center wrap">
|
||||
<paper-dropdown-menu label="Operation" class="operationMenu">
|
||||
<paper-dropdown-menu label="Operation" class="operationMenu" disabled="{{cantEdit}}">
|
||||
<paper-listbox class="dropdown-content" selected={{operationIndex operation}}>
|
||||
<paper-item label="Base Value" name="base"> Base Value </paper-item>
|
||||
<paper-item label="Add" name="add"> Add </paper-item>
|
||||
@@ -47,21 +49,21 @@
|
||||
<paper-item label="Conditional" name="conditional"> Conditional </paper-item>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-input class="LibraryItemEffectStat flex" label="Attribute" value={{stat}}></paper-input>
|
||||
<paper-input class="LibraryItemEffectValue flex" label="Value" value={{calculationOrValue}}></paper-input>
|
||||
<paper-icon-button icon="delete" class="deleteEffect"></paper-icon-button>
|
||||
<paper-input class="LibraryItemEffectStat flex" label="Attribute" value={{stat}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input class="LibraryItemEffectValue flex" label="Value" value={{calculationOrValue}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-icon-button icon="delete" class="deleteEffect" disabled="{{cantEdit}}"></paper-icon-button>
|
||||
</div>
|
||||
{{/each}}
|
||||
<paper-button id="addEffect" class="red-button">Add Effect</paper-button>
|
||||
<paper-button id="addEffect" class="red-button" disabled="{{cantEdit}}">Add Effect</paper-button>
|
||||
</div>
|
||||
<div style="margin-top: 8px;">
|
||||
<div class="paper-font-subhead">Attacks</div>
|
||||
{{#each indexedAttacks}}
|
||||
<div class="effect layout horizontal center wrap">
|
||||
<paper-input class="LibraryItemAttackBonusInput flex" label="Attack Bonus" value={{attackBonus}}></paper-input>
|
||||
<paper-input class="LibraryItemAttackDamageInput flex" label="Damage" value={{damage}}></paper-input>
|
||||
<paper-input class="LibraryItemAttackDetailsInput flex" label="Details" value={{details}}></paper-input>
|
||||
<paper-dropdown-menu label="Damage Type" class="damageTypeMenu">
|
||||
<paper-input class="LibraryItemAttackBonusInput flex" label="Attack Bonus" value={{attackBonus}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input class="LibraryItemAttackDamageInput flex" label="Damage" value={{damage}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-input class="LibraryItemAttackDetailsInput flex" label="Details" value={{details}} disabled="{{cantEdit}}"></paper-input>
|
||||
<paper-dropdown-menu label="Damage Type" class="damageTypeMenu" disabled="{{cantEdit}}">
|
||||
<paper-listbox class="dropdown-content" selected={{damageTypeIndex damageType}}>
|
||||
<paper-item label="Bludgeoning" name="bludgeoning"> Bludgeoning </paper-item>
|
||||
<paper-item label="Piercing" name="piercing"> Piercing </paper-item>
|
||||
@@ -78,11 +80,14 @@
|
||||
<paper-item label="Thunder" name="thunder"> Thunder </paper-item>
|
||||
</paper-listbox>
|
||||
</paper-dropdown-menu>
|
||||
<paper-icon-button icon="delete" class="deleteAttack"></paper-icon-button>
|
||||
<paper-icon-button icon="delete" class="deleteAttack" disabled="{{cantEdit}}"></paper-icon-button>
|
||||
</div>
|
||||
{{/each}}
|
||||
<paper-button id="addAttack" class="red-button">Add Attack</paper-button>
|
||||
<paper-button id="addAttack" class="red-button" disabled="{{cantEdit}}">Add Attack</paper-button>
|
||||
</div>
|
||||
{{else}}
|
||||
<paper-spinner active></paper-spinner>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,3 +1,9 @@
|
||||
Template.libraryItemDialog.onCreated(function(){
|
||||
this.autorun(() => {
|
||||
this.subscribe('libraryItem', Template.currentData().itemId);
|
||||
});
|
||||
});
|
||||
|
||||
Template.libraryItemDialog.helpers({
|
||||
item(){
|
||||
return LibraryItems.findOne(this.itemId);
|
||||
@@ -55,6 +61,20 @@ Template.libraryItemDialog.helpers({
|
||||
thunder: 12,
|
||||
};
|
||||
return ref[damageType];
|
||||
},
|
||||
ready(){
|
||||
return Template.instance().subscriptionsReady();
|
||||
},
|
||||
cantEdit(){
|
||||
let item = LibraryItems.findOne(this.itemId);
|
||||
if (!item) return;
|
||||
let library = Libraries.findOne(item.library);
|
||||
if (!library) return;
|
||||
let userId = Meteor.userId();
|
||||
return !(
|
||||
library.owner === userId ||
|
||||
_.contains(library.writers, userId)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
27
app/client/views/patreon/patronsOnly.html
Normal file
27
app/client/views/patreon/patronsOnly.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<template name="patronsOnly">
|
||||
<div class="flex layout vertical center" style="position: relative; padding: 0 16px;">
|
||||
<paper-material class="card" style="padding: 32px; max-width: 800px; width: 100%;">
|
||||
<h3>
|
||||
This beta feature is available to Patreon Insiders who pledge $5 or more
|
||||
</h3>
|
||||
<div class="layout vertical center">
|
||||
<a href="https://www.patreon.com/join/dicecloud/checkout?rid=3002853">
|
||||
<paper-button raised> Become a Patron </paper-button>
|
||||
</a>
|
||||
<a href="{{patreonLoginUrl}}">
|
||||
<paper-button class="connectPatreon" style="color: #d13b2e; margin-top: 12px;">
|
||||
Connect Patreon account
|
||||
</paper-button>
|
||||
</a>
|
||||
</div>
|
||||
<p style="margin-top: 32px;">
|
||||
With the Item Libraries beta you can create collections of items to use
|
||||
across your characters, and share them with other players.
|
||||
</p>
|
||||
<p>
|
||||
You can also subscribe to existing community libraries of items, saving
|
||||
time and effort manually entering item details.
|
||||
</p>
|
||||
</paper-material>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,5 @@
|
||||
import { format as formatUrl } from 'url';
|
||||
|
||||
const CLIENT_ID = Meteor.settings.public.patreon.clientId;
|
||||
|
||||
Template.profile.onCreated(function(){
|
||||
this.showApiKey = new ReactiveVar(false);
|
||||
this.loadingPatreon = new ReactiveVar(false);
|
||||
@@ -17,20 +15,6 @@ Template.profile.helpers({
|
||||
showApiKey: function(){
|
||||
return Template.instance().showApiKey.get();
|
||||
},
|
||||
patreonLoginUrl: function(){
|
||||
return formatUrl({
|
||||
protocol: 'https',
|
||||
host: 'patreon.com',
|
||||
pathname: '/oauth2/authorize',
|
||||
query: {
|
||||
response_type: 'code',
|
||||
client_id: CLIENT_ID,
|
||||
redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect',
|
||||
state: Meteor.userId(),
|
||||
scope: 'identity',
|
||||
},
|
||||
});
|
||||
},
|
||||
patreon: function(){
|
||||
let user = Meteor.user();
|
||||
return user && user.patreon || {};
|
||||
|
||||
68
app/package-lock.json
generated
68
app/package-lock.json
generated
@@ -100,9 +100,9 @@
|
||||
}
|
||||
},
|
||||
"bower": {
|
||||
"version": "1.8.4",
|
||||
"resolved": "https://registry.npmjs.org/bower/-/bower-1.8.4.tgz",
|
||||
"integrity": "sha1-54dqB23rgTf30GUl3F6MZtuC8oo="
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/bower/-/bower-1.8.8.tgz",
|
||||
"integrity": "sha512-1SrJnXnkP9soITHptSO+ahx3QKp3cVzn8poI6ujqc5SeOkg5iqM1pK9H+DSc2OQ8SnO0jC/NG4Ur/UIwy7574A=="
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.1",
|
||||
@@ -1126,37 +1126,6 @@
|
||||
"requires": {
|
||||
"inherits": "~2.0.1",
|
||||
"readable-stream": "^2.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stream-http": {
|
||||
@@ -1169,37 +1138,6 @@
|
||||
"readable-stream": "^2.3.3",
|
||||
"to-arraybuffer": "^1.0.0",
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
|
||||
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@polymer/polymer": "^1.2.5-npm-test.2",
|
||||
"bcrypt": "^1.0.3",
|
||||
"bower": "^1.7.9",
|
||||
"bower": "^1.8.8",
|
||||
"core-js": "^2.5.7",
|
||||
"fibers": "^2.0.2",
|
||||
"file-saver": "^2.0.1",
|
||||
|
||||
@@ -70,4 +70,8 @@
|
||||
--paper-diff-slider-knob-color: #00BCD4;
|
||||
--paper-diff-slider-pin-color: #00BCD4;
|
||||
}
|
||||
.white-text paper-input {
|
||||
/* Input foreground color */
|
||||
--paper-input-container-input-color: rgba(255,255,255,0.87);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,256 +1,262 @@
|
||||
import request from 'request';
|
||||
|
||||
const CLIENT_ID = Meteor.settings.public.patreon.clientId;
|
||||
const CLIENT_SECRET = Meteor.settings.patreon.clientSecret;
|
||||
const CREATOR_ACCESS_TOKEN = Meteor.settings.patreon.creatorAccessToken;
|
||||
const CAMPAIGN_ID = Meteor.settings.public.patreon.campaignId;
|
||||
if (
|
||||
Meteor.settings &&
|
||||
Meteor.settings.public &&
|
||||
Meteor.settings.public.patreon
|
||||
) {
|
||||
const CLIENT_ID = Meteor.settings.public.patreon.clientId;
|
||||
const CLIENT_SECRET = Meteor.settings.patreon.clientSecret;
|
||||
const CREATOR_ACCESS_TOKEN = Meteor.settings.patreon.creatorAccessToken;
|
||||
const CAMPAIGN_ID = Meteor.settings.public.patreon.campaignId;
|
||||
|
||||
// Handle redirects from patreon
|
||||
Router.map(function () {
|
||||
this.route("patreon-redirect", {
|
||||
path: "/patreon-redirect",
|
||||
where: "server",
|
||||
action: function () {
|
||||
let route = this;
|
||||
let userId = route.params.query.state;
|
||||
let singleUseCode = route.params.query.code;
|
||||
requestToken(singleUseCode, Meteor.bindEnvironment((error, response, body) => {
|
||||
// Should return an access token, valid for 1 month, which needs to be
|
||||
// stored and used to make requests on behalf of the user
|
||||
if (error){
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = JSON.parse(body);
|
||||
writePatreonToken(userId, token);
|
||||
} catch(error) {
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
updateIdentity(token.access_token, userId);
|
||||
}));
|
||||
route.response.writeHead(302, {
|
||||
'Location': Meteor.absoluteUrl() + "account",
|
||||
});
|
||||
route.response.end();
|
||||
},
|
||||
// Handle redirects from patreon
|
||||
Router.map(function () {
|
||||
this.route("patreon-redirect", {
|
||||
path: "/patreon-redirect",
|
||||
where: "server",
|
||||
action: function () {
|
||||
let route = this;
|
||||
let userId = route.params.query.state;
|
||||
let singleUseCode = route.params.query.code;
|
||||
requestToken(singleUseCode, Meteor.bindEnvironment((error, response, body) => {
|
||||
// Should return an access token, valid for 1 month, which needs to be
|
||||
// stored and used to make requests on behalf of the user
|
||||
if (error){
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = JSON.parse(body);
|
||||
writePatreonToken(userId, token);
|
||||
} catch(error) {
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
updateIdentity(token.access_token, userId);
|
||||
}));
|
||||
route.response.writeHead(302, {
|
||||
'Location': Meteor.absoluteUrl() + "account",
|
||||
});
|
||||
route.response.end();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const requestToken = function(singleUseCode, callback){
|
||||
request({
|
||||
method: "POST",
|
||||
uri: "https://www.patreon.com/api/oauth2/token",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
qs: {
|
||||
code: singleUseCode,
|
||||
grant_type: "authorization_code",
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect',
|
||||
},
|
||||
}, callback);
|
||||
}
|
||||
const requestToken = function(singleUseCode, callback){
|
||||
request({
|
||||
method: "POST",
|
||||
uri: "https://www.patreon.com/api/oauth2/token",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
qs: {
|
||||
code: singleUseCode,
|
||||
grant_type: "authorization_code",
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
redirect_uri: Meteor.absoluteUrl() + 'patreon-redirect',
|
||||
},
|
||||
}, callback);
|
||||
}
|
||||
|
||||
const getIdentity = function(accessToken, callback){
|
||||
request({
|
||||
uri: "https://www.patreon.com/api/oauth2/v2/identity",
|
||||
headers:{
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
qs: {
|
||||
"include": "memberships",
|
||||
"fields[member]": "currently_entitled_amount_cents",
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
const getIdentity = function(accessToken, callback){
|
||||
request({
|
||||
uri: "https://www.patreon.com/api/oauth2/v2/identity",
|
||||
headers:{
|
||||
Authorization: "Bearer " + accessToken,
|
||||
},
|
||||
qs: {
|
||||
"include": "memberships",
|
||||
"fields[member]": "currently_entitled_amount_cents",
|
||||
}
|
||||
}, callback);
|
||||
};
|
||||
|
||||
// Should return a new access token for the user
|
||||
// callback is called with (error, response, body)
|
||||
const refreshAccessToken = Meteor.wrapAsync(function(refreshToken, userId, callback){
|
||||
request({
|
||||
method: "POST",
|
||||
uri: "https://www.patreon.com/api/oauth2/token",
|
||||
qs: {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}
|
||||
}, Meteor.bindEnvironment((error, response, body) => {
|
||||
// Should return an access token, valid for 1 month, which needs to be
|
||||
// stored and used to make requests on behalf of the user
|
||||
if (error){
|
||||
callback(error)
|
||||
return;
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = JSON.parse(body);
|
||||
writePatreonToken(userId, token);
|
||||
callback(undefined, token.access_token);
|
||||
} catch(error) {
|
||||
callback(error);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
const updateIdentity = Meteor.wrapAsync(function(accessToken, userId, callback){
|
||||
getIdentity(accessToken, Meteor.bindEnvironment((error, response, body) => {
|
||||
if (error){
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let identity = JSON.parse(body);
|
||||
let membership = identity.included[0];
|
||||
let entitledAmount = membership && membership.attributes
|
||||
.currently_entitled_amount_cents || 0;
|
||||
let patreonUserId = identity.data.id;
|
||||
writeEntitledCentsAndId(userId, entitledAmount, patreonUserId);
|
||||
if (callback) callback();
|
||||
} catch(error) {
|
||||
writePatreonError(userId, error);
|
||||
if(callback) callback(error);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
updateMyPatreonDetails(){
|
||||
const userId = this.userId;
|
||||
if (!userId) throw new Meteor.Error("not-logged-in", "You must be logged in to update Patreon details");
|
||||
const user = Meteor.users.findOne(userId, {fields: {patreon: 1}});
|
||||
Meteor.users.update(userId, {$unset: {"patreon.entitledCents": 1}});
|
||||
if (!user.patreon || !user.patreon.accessToken){
|
||||
throw new Meteor.Error("no-patreon-access", "Patreon access token not found for this user");
|
||||
}
|
||||
let accessToken = user.patreon.accessToken;
|
||||
if (user.patreon.tokenExpiryDate < new Date()){
|
||||
// Token expired, refresh it before continuing
|
||||
accessToken = refreshAccessToken(user.patreon.refreshToken, userId);
|
||||
}
|
||||
updateIdentity(accessToken, userId);
|
||||
},
|
||||
});
|
||||
|
||||
const writePatreonToken = function(userId, {
|
||||
access_token, refresh_token, expires_in
|
||||
}){
|
||||
// The expiry date is now plus `expires_in` seconds
|
||||
let expiryDate = new Date();
|
||||
expiryDate.setSeconds(expiryDate.getSeconds() + expires_in);
|
||||
// Expire a day early so we don't accidentally miss it
|
||||
expiryDate.setDate(expiryDate.getDate() - 1);
|
||||
|
||||
// Write
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.accessToken": access_token,
|
||||
"patreon.refreshToken": refresh_token,
|
||||
"patreon.tokenExpiryDate": expiryDate,
|
||||
},
|
||||
$unset: {
|
||||
"patreon.error": 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writeEntitledCentsAndId = function(userId, amount, patreonUserId){
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.entitledCents": amount,
|
||||
"patreon.userId": patreonUserId,
|
||||
},
|
||||
$unset: {
|
||||
"patreon.error": 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writePatreonError = function(userId, error){
|
||||
console.error({patreonError: error});
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.error": error.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const requestMembers = Meteor.wrapAsync(function(cursor, members, callback){
|
||||
request({
|
||||
uri: `https://www.patreon.com/api/oauth2/v2/campaigns/${CAMPAIGN_ID}/members`,
|
||||
headers:{
|
||||
Authorization: "Bearer " + CREATOR_ACCESS_TOKEN,
|
||||
},
|
||||
qs: {
|
||||
"include": "user",
|
||||
"fields[member]": "currently_entitled_amount_cents",
|
||||
"page[cursor]": cursor,
|
||||
}
|
||||
}, (error, reponse, body) => {
|
||||
if (error){
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
let json = JSON.parse(body);
|
||||
if (json.errors) {
|
||||
callback(json.errors);
|
||||
return;
|
||||
}
|
||||
let newMembers = json.data.map(member => ({
|
||||
id: member.relationships.user.data.id,
|
||||
entitledCents: member.attributes.currently_entitled_amount_cents,
|
||||
// Should return a new access token for the user
|
||||
// callback is called with (error, response, body)
|
||||
const refreshAccessToken = Meteor.wrapAsync(function(refreshToken, userId, callback){
|
||||
request({
|
||||
method: "POST",
|
||||
uri: "https://www.patreon.com/api/oauth2/token",
|
||||
qs: {
|
||||
grant_type: "refresh_token",
|
||||
refresh_token: refreshToken,
|
||||
client_id: CLIENT_ID,
|
||||
client_secret: CLIENT_SECRET,
|
||||
}
|
||||
}, Meteor.bindEnvironment((error, response, body) => {
|
||||
// Should return an access token, valid for 1 month, which needs to be
|
||||
// stored and used to make requests on behalf of the user
|
||||
if (error){
|
||||
callback(error)
|
||||
return;
|
||||
}
|
||||
let token;
|
||||
try {
|
||||
token = JSON.parse(body);
|
||||
writePatreonToken(userId, token);
|
||||
callback(undefined, token.access_token);
|
||||
} catch(error) {
|
||||
callback(error);
|
||||
}
|
||||
}));
|
||||
members.push(...newMembers);
|
||||
let next = json.meta.pagination.cursors && json.meta.pagination.cursors.next;
|
||||
if (next){
|
||||
callback(undefined, next);
|
||||
} else {
|
||||
callback(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const updatePatreonMembersEntitledCents = function(){
|
||||
let next = "";
|
||||
let members = [];
|
||||
do {
|
||||
next = requestMembers(next, members);
|
||||
} while (next)
|
||||
members.forEach(({id, entitledCents}) => {
|
||||
Meteor.users.update({
|
||||
"patreon.userId": id
|
||||
}, {$set: {
|
||||
"patreon.entitledCents":entitledCents,
|
||||
}});
|
||||
const updateIdentity = Meteor.wrapAsync(function(accessToken, userId, callback){
|
||||
getIdentity(accessToken, Meteor.bindEnvironment((error, response, body) => {
|
||||
if (error){
|
||||
writePatreonError(userId, error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let identity = JSON.parse(body);
|
||||
let membership = identity.included[0];
|
||||
let entitledAmount = membership && membership.attributes
|
||||
.currently_entitled_amount_cents || 0;
|
||||
let patreonUserId = identity.data.id;
|
||||
writeEntitledCentsAndId(userId, entitledAmount, patreonUserId);
|
||||
if (callback) callback();
|
||||
} catch(error) {
|
||||
writePatreonError(userId, error);
|
||||
if(callback) callback(error);
|
||||
}
|
||||
}));
|
||||
});
|
||||
return members;
|
||||
|
||||
Meteor.methods({
|
||||
updateMyPatreonDetails(){
|
||||
const userId = this.userId;
|
||||
if (!userId) throw new Meteor.Error("not-logged-in", "You must be logged in to update Patreon details");
|
||||
const user = Meteor.users.findOne(userId, {fields: {patreon: 1}});
|
||||
Meteor.users.update(userId, {$unset: {"patreon.entitledCents": 1}});
|
||||
if (!user.patreon || !user.patreon.accessToken){
|
||||
throw new Meteor.Error("no-patreon-access", "Patreon access token not found for this user");
|
||||
}
|
||||
let accessToken = user.patreon.accessToken;
|
||||
if (user.patreon.tokenExpiryDate < new Date()){
|
||||
// Token expired, refresh it before continuing
|
||||
accessToken = refreshAccessToken(user.patreon.refreshToken, userId);
|
||||
}
|
||||
updateIdentity(accessToken, userId);
|
||||
},
|
||||
});
|
||||
|
||||
const writePatreonToken = function(userId, {
|
||||
access_token, refresh_token, expires_in
|
||||
}){
|
||||
// The expiry date is now plus `expires_in` seconds
|
||||
let expiryDate = new Date();
|
||||
expiryDate.setSeconds(expiryDate.getSeconds() + expires_in);
|
||||
// Expire a day early so we don't accidentally miss it
|
||||
expiryDate.setDate(expiryDate.getDate() - 1);
|
||||
|
||||
// Write
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.accessToken": access_token,
|
||||
"patreon.refreshToken": refresh_token,
|
||||
"patreon.tokenExpiryDate": expiryDate,
|
||||
},
|
||||
$unset: {
|
||||
"patreon.error": 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writeEntitledCentsAndId = function(userId, amount, patreonUserId){
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.entitledCents": amount,
|
||||
"patreon.userId": patreonUserId,
|
||||
},
|
||||
$unset: {
|
||||
"patreon.error": 1,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const writePatreonError = function(userId, error){
|
||||
console.error({patreonError: error});
|
||||
Meteor.users.update(userId, {
|
||||
$set: {
|
||||
"patreon.error": error.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const requestMembers = Meteor.wrapAsync(function(cursor, members, callback){
|
||||
request({
|
||||
uri: `https://www.patreon.com/api/oauth2/v2/campaigns/${CAMPAIGN_ID}/members`,
|
||||
headers:{
|
||||
Authorization: "Bearer " + CREATOR_ACCESS_TOKEN,
|
||||
},
|
||||
qs: {
|
||||
"include": "user",
|
||||
"fields[member]": "currently_entitled_amount_cents",
|
||||
"page[cursor]": cursor,
|
||||
}
|
||||
}, (error, reponse, body) => {
|
||||
if (error){
|
||||
callback(error);
|
||||
return;
|
||||
}
|
||||
let json = JSON.parse(body);
|
||||
if (json.errors) {
|
||||
callback(json.errors);
|
||||
return;
|
||||
}
|
||||
let newMembers = json.data.map(member => ({
|
||||
id: member.relationships.user.data.id,
|
||||
entitledCents: member.attributes.currently_entitled_amount_cents,
|
||||
}));
|
||||
members.push(...newMembers);
|
||||
let next = json.meta.pagination.cursors && json.meta.pagination.cursors.next;
|
||||
if (next){
|
||||
callback(undefined, next);
|
||||
} else {
|
||||
callback(undefined);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const updatePatreonMembersEntitledCents = function(){
|
||||
let next = "";
|
||||
let members = [];
|
||||
do {
|
||||
next = requestMembers(next, members);
|
||||
} while (next)
|
||||
members.forEach(({id, entitledCents}) => {
|
||||
Meteor.users.update({
|
||||
"patreon.userId": id
|
||||
}, {$set: {
|
||||
"patreon.entitledCents":entitledCents,
|
||||
}});
|
||||
});
|
||||
return members;
|
||||
}
|
||||
|
||||
// Method to run a manual update
|
||||
Meteor.methods({
|
||||
updatePatreonMembersEntitledCents(){
|
||||
const user = Meteor.users.findOne(this.userId);
|
||||
if (!user || !_.contains(user.roles, "admin")) throw new Meteor.Error(
|
||||
"permission-error", "You need to be logged in as an admin to run this method"
|
||||
);
|
||||
return updatePatreonMembersEntitledCents();
|
||||
},
|
||||
});
|
||||
|
||||
// Cron job to run the update automatically
|
||||
Meteor.startup(() => {
|
||||
SyncedCron.add({
|
||||
name: "updatePatreonMembersEntitledCents",
|
||||
schedule: function(parser) {
|
||||
return parser.text('every 4 hours');
|
||||
},
|
||||
job: updatePatreonMembersEntitledCents,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Method to run a manual update
|
||||
Meteor.methods({
|
||||
updatePatreonMembersEntitledCents(){
|
||||
const user = Meteor.users.findOne(this.userId);
|
||||
if (!user || !_.contains(user.roles, "admin")) throw new Meteor.Error(
|
||||
"permission-error", "You need to be logged in as an admin to run this method"
|
||||
);
|
||||
return updatePatreonMembersEntitledCents();
|
||||
},
|
||||
});
|
||||
|
||||
// Cron job to run the update automatically
|
||||
Meteor.startup(() => {
|
||||
SyncedCron.add({
|
||||
name: "updatePatreonMembersEntitledCents",
|
||||
schedule: function(parser) {
|
||||
return parser.text('every 4 hours');
|
||||
},
|
||||
job: updatePatreonMembersEntitledCents,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -25,16 +25,61 @@ Meteor.publish("standardLibrarySpells", function(level){
|
||||
});
|
||||
|
||||
Meteor.publish("customLibraries", function(){
|
||||
userId = this.userId;
|
||||
const userId = this.userId;
|
||||
let user = Meteor.user()
|
||||
let subs = user && user.profile && user.profile.librarySubscriptions;
|
||||
return Libraries.find({
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
{public: true, _id: {$in: subs || []}},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish("singleLibrary", function(id){
|
||||
const userId = this.userId;
|
||||
return Libraries.find({
|
||||
_id: id,
|
||||
$or: [
|
||||
{readers: userId},
|
||||
{writers: userId},
|
||||
{owner: userId},
|
||||
{public: true},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish("libraryItems", function(libraryId){
|
||||
return LibraryItems.find({library: libraryId});
|
||||
return LibraryItems.find({
|
||||
library: libraryId
|
||||
}, {
|
||||
fields: {
|
||||
name: 1,
|
||||
libraryName: 1,
|
||||
library: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Meteor.publish("libraryItem", function(itemId){
|
||||
let cursor = LibraryItems.find(itemId);
|
||||
let item = cursor.fetch()[0];
|
||||
let userId = Meteor.userId();
|
||||
if (!item) return [];
|
||||
let library = Libraries.findOne(item.library);
|
||||
if (!library) {
|
||||
throw new Meteor.Error("Library item " + item._id + " is an orphan");
|
||||
}
|
||||
if (
|
||||
library.public ||
|
||||
library.owner === userId ||
|
||||
_.contains(library.readers, userId) ||
|
||||
_.contains(library.writers, userId)
|
||||
) {
|
||||
return cursor;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
// This all gets run in the console by an admin.
|
||||
// Probably a good idea to reset the server after running big updates
|
||||
// Only do if the library doesn't exist yet
|
||||
|
||||
// First Setup
|
||||
// -----------
|
||||
|
||||
// Add the SRD library with the correct static ID:
|
||||
id = Libraries.insert({
|
||||
_id: "SRDLibraryGA3XWsd",
|
||||
owner: Meteor.userId(),
|
||||
@@ -8,19 +11,23 @@ id = Libraries.insert({
|
||||
});
|
||||
|
||||
// First copy-paste the JSON into your console like `items = <pasted JSON>`
|
||||
// First import, don't do this if the library is already populated
|
||||
_.each(items, (item) => {
|
||||
item.settings = {category: }; // "adventuringGear", "armor", "weapons", "tools"
|
||||
// replace "adventuringGear" with appropriate category: "armor", "weapons", "tools"
|
||||
// if needed
|
||||
item.settings = {category: "adventuringGear"};
|
||||
item.library = "SRDLibraryGA3XWsd"
|
||||
LibraryItems.insert(item)
|
||||
});
|
||||
|
||||
// First copy-paste the JSON into your console like `spells = <pasted JSON>`
|
||||
_.each(spells, (spell) => {
|
||||
spell.library = "SRDLibraryGA3XWsd"
|
||||
LibrarySpells.insert(spell)
|
||||
});
|
||||
|
||||
// Update the library using names as keys
|
||||
// Updating the Libary
|
||||
// -------------------
|
||||
|
||||
// Make sure you're subscribed to all item categories
|
||||
handles = _.map(["weapons", "armor", "adventuringGear", "tools"],
|
||||
category => Meteor.subscribe("standardLibraryItems", category)
|
||||
|
||||
Reference in New Issue
Block a user