Compare commits

...

3 Commits

Author SHA1 Message Date
Max
276a72b4ce Gigantic Update
* Migrated manual tests to jest and started working on better coverage
* Added a bookmarklet and push key generation tool allowing URL pushing from bookmarklets
* Updated web scraping with tons of bug fixes
* Updated attachments page to handle new push links
* Aggressive note change checking, if patches get out of sync, server overwrites bad updates.
2023-10-17 19:46:14 +00:00
Max
b5ef64485f + Giant update to multiple users editing notes.
- Edits are done per DOM element, making diffs smaller and faster
- Multiple users can now edit the same note witheout it turning into a gaint mess
- Caret position is saved better and wont jump around as much

+ Removed Active sessions text
2023-07-30 04:18:17 +00:00
Max
c61f0c0198 Graph update and little noe ui tweaks 2023-07-23 23:13:28 +00:00
35 changed files with 9002 additions and 734 deletions

View File

@@ -16,6 +16,9 @@ gzip "backup-$NOW.sql"
echo "Database Backup Complete on $NOW"
# Delete all but last 8 files
ls -tp | grep -v '/$' | tail -n +9 | tr '\n' '\0' | xargs -0 rm --
##
# Restore DB
##

View File

@@ -17,13 +17,14 @@
body {
margin: 0;
padding: 0;
overflow-x: hidden;
/* overflow-x: hidden;*/
min-width: 320px;
background: #FFFFFF;
background: green;
font-family: 'Roboto', system-ui, -apple-system, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 14px;
line-height: 1.4285em;
color: rgba(0, 0, 0, 0.87);
position: relative;
}
:root {
@@ -95,7 +96,7 @@ body {
font-family: 'Roboto', 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
#app {
background: var(--body_bg_color);
/* background: var(--body_bg_color);*/
}
.ui.segment {
@@ -591,6 +592,15 @@ padding-right: 10px;
color: var(--main-accent);
opacity: 1;
}
/* Remove indent line on mobile */
.note-card-text > ol > ol,
.squire-box > ol > ol,
.note-card-text > ul > ul,
.squire-box > ul > ul
{
border-left: none;
}
}

View File

@@ -117,11 +117,16 @@
<a class="link" :href="linkUrl" target="_blank">{{linkText}}</a>
<!-- Buttons -->
<div class="ui small compact basic button" v-on:click="openNote">
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openNote">
<i class="file outline icon"></i>
Open Note
</div>
<div class="ui small compact basic button" v-on:click="openEditAttachments"
<div v-if="!item.note_id" class="ui small compact basic disabled button">
<i class="angle double up icon"></i>
Pushed from Web
</div>
<div v-if="item.note_id" class="ui small compact basic button" v-on:click="openEditAttachments"
:class="{ 'disabled':this.searchParams.noteId }">
<i class="folder open outline icon"></i>
Note Files

View File

@@ -87,6 +87,7 @@
margin: 0;
padding: 0;
overflow: hidden;
}
.place-holder {
width: 100%;

View File

@@ -4,7 +4,7 @@
<div>
<!-- thicc form display -->
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register()">
<div v-if="!thin" class="ui large form" v-on:keyup.enter="register">
<div class="field">
<div class="ui input">
<input ref="nameForm" v-model="username" type="text" name="email" placeholder="Username or E-mail">
@@ -15,6 +15,11 @@
<input v-model="password" type="password" name="password" placeholder="Password">
</div>
</div>
<div class="field">
<div class="ui input">
<input v-model="password2" type="password" name="password2" placeholder="Re-type Password">
</div>
</div>
<div class="field" v-if="require2FA">
<div class="ui input">
<input v-model="authToken" ref="authForm" type="text" name="authToken" placeholder="Authorization Token">
@@ -24,17 +29,10 @@
<div class="ui fluid buttons">
<div v-on:click="register()" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
<div v-on:click="register" class="ui green button" :class="{ 'disabled':(username.length == 0 || password.length == 0)}">
<i class="plug icon"></i>
Sign Up
</div>
<div class="or"></div>
<div :class="{ 'disabled':(username.length == 0 || password.length == 0)}" v-on:click="login()" class="ui button">
<i class="power icon"></i>
Login
</div>
</div>
</div>
@@ -49,12 +47,12 @@
</div>
<!-- Thin form display -->
<div v-if="thin" class="ui small form" v-on:keyup.enter="login()">
<div v-if="thin" class="ui small form" v-on:keyup.enter="login">
<div v-if="!require2FA" class="field"><!-- hide this field if someone is logging in with 2FA -->
<div class="ui grid">
<div class="ui sixteen wide center aligned column">
<div v-on:click="register()" class="ui green button">
<div v-on:click="register" class="ui green button">
<i class="plug icon"></i>
Sign Up Now!
</div>
@@ -87,7 +85,7 @@
</div>
</div>
<div class="field">
<div v-on:click="login()" class="ui fluid button">
<div v-on:click="login" class="ui fluid button">
<i class="power icon"></i>
Login
</div>
@@ -128,6 +126,7 @@
enabled: false,
username: '',
password: '',
password2: '',
authToken: '',
require2FA: false,
}
@@ -160,13 +159,21 @@
},
register(){
if( this.username.length == 0 || this.password.length == 0 ){
let error = false
if(this.$route.name == 'LoginPage'){
this.$bus.$emit('notification', 'Both a Username and Password are Required')
return
}
if( this.username.length == 0 || this.password.length == 0 || this.password2.length == 0 ){
this.$bus.$emit('notification', 'All fields are required.')
error = true
}
if( this.password !== this.password2 ){
this.$bus.$emit('notification', 'Passwords must be identical.')
error = true
}
if(error){
//Login section
this.$router.push('/login')
return

View File

@@ -0,0 +1,431 @@
<style type="text/css" scoped>
.an-graph {
background: #fefefe;
}
.inactive.segment {
}
.active.segment {
outline: 4px solid cyan;
outline-offset: -5px;
outline-style: dashed;
max-height: 2000px;
}
.not-padded {
margin-left: -5px;
margin-right: -5px;
margin-bottom: -10px;
padding-right: 5px;
padding-left: 5px;
}
.sticky-boy {
position: fixed;
top: -1px;
right: 10px;
z-index: 100;
width: 70%;
background: orange;
}
.animate-height {
transition: max-height 0.8s linear;
max-height: 450px;
overflow: hidden;
}
</style>
<template>
<div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<div v-for="(graph, index) in graphs" :class="`ui not-padded ${editGraphs?'active ':'inactive '}segment animate-height`">
<!-- Edit options -->
<div class="ui small header" v-if="editGraphs">
<div class="ui grid">
<div class="eight wide column">
<b>Graph #{{ index+1 }}</b>
</div>
<div class="eight wide right aligned column">
<span class="ui tiny compact inverted red button" v-on:click="removeGraph(index)">
Remove Graph
<i class="close icon"></i>
</span>
</div>
</div>
</div>
<h3 class="ui center aligned dividing header">
{{ getGraphTitle(graph) }}
</h3>
<div v-if="graph?.type == PILL_CALENDAR">
<PillCalendarGraph
:graph="graph"
:tempChartDays="tempChartDays"
:userFields="userFields"
:cycleData="cycleData"
:edit-graphs="editGraphs"
:showZeroValues="graph?.options?.showZeroValues"
:showTextValues="graph?.options?.showTextValues"
:connectDays="graph?.options?.connectDays"
:hideValues="graph?.options?.hideValues"
:hideIcons="graph?.options?.hideIcons"
/>
<div v-if="editGraphs" class="ui segment">
<p>Calendar Graph Toggles</p>
<div v-on:click="toggelValue(index, 'hideIcons')"class="ui button">
<span v-if="graph?.options?.hideIcons">Show</span><span v-else>Hide</span> Icons
</div>
<div v-on:click="toggelValue(index, 'hideValues')"class="ui button">
<span v-if="graph?.options?.hideValues">Show</span><span v-else>Hide</span> Values
</div>
<div v-on:click="toggelValue(index, 'showZeroValues')"class="ui button">
<span v-if="!graph?.options?.showZeroValues">Show</span><span v-else>Hide</span> Lowest Value
</div>
<div v-on:click="toggelValue(index, 'showTextValues')"class="ui button">
<span v-if="!graph?.options?.showTextValues">Show</span><span v-else>Hide</span> Text Value
</div>
<div v-on:click="toggelValue(index, 'connectDays')"class="ui button">
<span v-if="!graph?.options?.connectDays">Connect</span><span v-else>Disconnect</span> Days
</div>
</div>
</div>
<div v-if="graph?.type == LAST_DONE">
Last done not implemented
</div>
<div v-if="!graph.fieldIds || graph.fieldIds && graph.fieldIds.length == 0">
<h5>Blank Graph</h5>
<span v-if="!editGraphs">Click "Edit Graphs" then,</span>
Select Graph type and Metrics to display
</div>
<div v-if="graph?.type == undefined && graph.fieldIds && graph.fieldIds.length > 0">
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
</div>
<div class="ui segment" v-if="editGraphs">
<!-- change graph type -->
<div v-for="(graphType, graphId) in graphTypesDef" class="ui buttons">
<div class="ui tiny button" v-on:click="changeGraphType(index, graphId)" :class="{'green':(String(graphId) == String(graph?.type))}">
{{ graphType }}
</div>
</div>
<div v-for="fieldId in fields">
<span v-if="graph.fieldIds && graph.fieldIds.includes(fieldId)" v-on:click="toggleGraphField(fieldId, index)">
<i class="green check square icon"></i>
</span>
<span v-else v-on:click="toggleGraphField(fieldId, index)">
<i class="square outline icon"></i>
</span>
<i :class="`${$parent.getFieldColor(fieldId)} ${$parent.getFieldIcon(fieldId)} icon`"></i>
<b>{{ userFields[fieldId]?.label }}</b>
</div>
</div>
</div>
<div class="ui very compact grid" :class="{'sticky-boy':editGraphs}">
<div class="sixteen wide column" v-if="!editGraphs">
<div class="ui basic padded segment">
<!-- Just a space to keep things clickable -->
</div>
</div>
<div class="sixteen wide column">
<dix class="ui basic segment" v-if="!editGraphs">
<div class="ui button" v-on:click="toggleEditGraphs">
<i class="edit icon"></i>
<span>Add/Edit Graphs</span>
</div>
</dix>
<div v-if="editGraphs">
<div class="ui green button" v-on:click="addGraph()">
<i class="plus icon"></i>
New Graph
</div>
<div class="ui basic button" v-on:click="toggleEditGraphs">
<i class="check circle icon"></i>
Done Editing Graphs
</div>
</div>
</div>
</div>
<!-- Anchor for scrolling to the bottom of graphs -->
<div ref="anchor"></div>
</div>
</template>
<script>
const PILL_CALENDAR = 'pillCalendar'
const LAST_DONE = 'lastDone'
export default {
name: 'MetricTrackingGraphs',
props: [
'tempChartDays', // Number of days to display
'fields', // field IDs for display/order
'userFields', // field values defined by user
'graphs', // Graph data defined by user
'cycleData', // ALL user data
'calendar', // Date data for currently open day
'editGraphs' // boolean for edit or not edit graphs
],
components: {
'PillCalendarGraph':require('@/components/Metrictracking/PillCalendarGraph.vue').default,
},
data: function(){
return {
graphTypesDef:{
// [LAST_DONE]: 'Last Done',
'undefined':'Line Graph (Default)',
[PILL_CALENDAR]:'Calendar Graph',
},
localGraphData:[],
}
},
beforeCreate() {
// Constants
this.PILL_CALENDAR = PILL_CALENDAR
this.LAST_DONE = LAST_DONE
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
},
mounted(){
this.localGraphData = this.graphs
this.graphCurrentData()
},
updated(){
// update graphs here? Or watch graphs prop
},
watch: {
// whenever question changes, this function will run
userFields(newFields, oldFields) {
// console.log([newFields, oldFields])
if( JSON.stringify(oldFields) == "{}" ){
this.graphCurrentData()
}
},
tempChartDays(newDays, oldDays){
if( newDays != oldDays ){
this.graphCurrentData()
}
},
},
methods: {
saveGraphs(){
this.$emit('saveGraphs', this.localGraphData)
},
toggleEditGraphs(){
setTimeout(() => {
// scroll last graph into view
this.$refs.anchor.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
}, 800)
this.$emit('toggleEditGraphs')
},
changeGraphType(index, newType){
console.log(index + ' change to ' + newType)
this.localGraphData[index]['type'] = newType
this.saveGraphs()
},
addGraph(){
this.localGraphData.push({})
this.saveGraphs()
},
removeGraph(index){
this.localGraphData.splice(index, 1)
this.saveGraphs()
},
toggelValue(graphIndex, optionName){
if(!this.localGraphData[graphIndex].options){
this.localGraphData[graphIndex].options = {}
}
if(this.localGraphData[graphIndex].options[optionName]){
this.localGraphData[graphIndex].options[optionName] = false
}
else {
this.localGraphData[graphIndex].options[optionName] = true
}
console.log(this.localGraphData[graphIndex].options[optionName])
this.saveGraphs()
},
toggleGraphField(fieldId, graphIndex){
if(!Array.isArray(this.localGraphData[graphIndex].fieldIds)){
this.localGraphData[graphIndex].fieldIds = []
}
const inSetCheck = this.localGraphData[graphIndex]?.fieldIds.indexOf(fieldId)
if(inSetCheck == -1){
this.localGraphData[graphIndex]?.fieldIds.push(fieldId)
}
if(inSetCheck > -1){
this.localGraphData[graphIndex]?.fieldIds.splice(inSetCheck,1)
}
this.saveGraphs()
},
getGraphTitle(graph){
const graphFields = graph?.fieldIds || []
let fieldTitles = []
graphFields.forEach(fieldId => {
fieldTitles.push(this.userFields[fieldId]?.label)
})
// console.log(fieldTitles)
const title = fieldTitles.join(', ')
return title
},
graphCurrentData(){
// try again if dygraphs isn't loaded
if( typeof(window.Dygraph) != 'function' ){
setTimeout(() => {
this.graphCurrentData()
}, 100)
return
}
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys = dataKeys.splice(0, this.tempChartDays)
console.log(dataKeys)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// only chart line graphs with dygraphs
if( graph.type != undefined ){
return
}
if( !graph.fieldIds ){
return
}
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
const escapedLabel = graphLabel.replaceAll(',','')
graphLabels.push(escapedLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
// setup correct float graphing
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if( currentValue == undefined ){
currentValue = -1
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
}
}
</script>

View File

@@ -1,26 +1,547 @@
<style type="text/css" scoped></style>
<style type="text/css" scoped>
div.calendar {
width: calc(100% - 4px);
min-height: 350px;
display: flex;
margin: 5px 8px 15px;
flex-wrap: wrap;
flex-direction: row;
justify-content: flex-start;
}
.day {
flex: 0 0 calc(14.28% - 2px);
min-height: 50px;
border: 1px solid var(--border_color);
font-size: 1.2em;
overflow: hidden;
box-sizing: border-box;
position: relative;
line-height: 1em;
display: flex;
align-items: flex-end;
}
.today {
font-weight: bold;
text-decoration: underline;
}
.active-entry {
outline: #07f4f4;
outline-style: none;
outline-width: medium;
outline-style: none;
outline-offset: -1px;
outline-style: solid;
outline-width: 3px;
}
.day ~ .has-data {
}
.day ~ .no-data {
background: #c7c7c787;
opacity: 0.6;
}
.day > .number {
position: absolute;
top: 0;
right: 5px;
z-index: 10;
opacity: 0.4;
}
.day > .sex {
font-size: 0.7em;
border-radius: 5px;
background: rgba(249, 0, 0, 0.15);
color: white;
padding: 0 0 0 4px;
z-index: 10;
position: absolute;
left: 0;
height: 26px;
}
.day > .period {
position: absolute;
bottom: 1px;
left: 1px;
right: 1px;
height: 5px;
background: red;
z-index: 10;
}
.day > .mucus {
position: absolute;
bottom: 0;
left: 0;
right: 0;
min-height: 10px;
background: #abecff7d;
z-index: 2;
}
.day > .notes {
}
.pill-container {
width: 100%;
}
.pill {
width: calc(100% - 8px);
min-height: 2px;
margin: 0 4px;
box-sizing: border-box;
display: inline-block;
background: rgb(50 218 255 / 44%);
border-radius: 40px;
text-align: center;
line-height: 1em;
position: relative;
color: white;
font-size: 0.7em;
padding: 2px;
overflow: hidden;
white-space: nowrap;
}
.pill.did-last {
margin-left: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next {
margin-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: calc(100% - 5px);
}
.pill.did-next.did-last {
width: 100%;
}
/* .last-high:after {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-left: 10px solid rgb(50 218 255 / 44%);
position: absolute;
left: 0;
top: -13px;
}
.next-high:before {
content: '';
width: 0;
height: 0;
border-top: 15px solid transparent;
border-bottom: 3px solid transparent;
border-right: 10px solid rgb(50 218 255 / 44%);
position: absolute;
right: 0;
top: -13px;
}*/
.big-day {
display: inline-block;
width: 100%;
min-height: 2px;
margin: 0 auto;
text-align: center;
}
.zero-day {
opacity: 0.5;
}
.icon-spacer {
display: inline-block;
background-color: greenyellow;
width: 20px;
height: 2px;
}
.past-entries {
width: 100%;
display: flex;
justify-content: space-around;
/* padding: 0 10px;*/
overflow-x: scroll;
overflow-y: hidden;
}
.past-entry {
position: relative;
text-align: center;
border: 1px solid;
border-color: var(--dark_border_color);
color: var(--text_color);
flex-grow: 1;
cursor: pointer;
font-weight: bold;
min-width: 40px;
min-height: 40px;
margin: 5px 0 10px;
line-height: 2.3em;
}
.day-list {
width: 100%;
height: 80px;
background-color: green;
display: flex;
justify-content: space-around;
overflow-x: scroll;
overflow-y: hidden;
}
.day-list-item {
flex-grow: 1;
border: 1px solid black;
width: 25px;
}
.pill.red { background-color: #db2828 }
.pill.orange { background-color: #f2711c }
.pill.yellow { background-color: #fbbd08 }
.pill.olive { background-color: #b5cc18 }
.pill.green { background-color: #21ba45 }
.pill.teal { background-color: #00b5ad }
.pill.blue { background-color: #2185d0 }
.pill.violet { background-color: #6435c9 }
.pill.purple { background-color: #a333c8 }
.pill.pink { background-color: #e03997 }
.pill.brown { background-color: #a5673f }
.pill.grey { background-color: #767676 }
.pill.black { background-color: #1b1c1d }
</style>
<template>
<div>
I'm a calednar yo
<div class="calendar">
<div v-for="day in calendar.weekdays" class="day">
{{ day }}
</div>
<div v-for="day in calendar.days" class="day"
:class="{
'today':day == calendar.today,
'active-entry':calendar.dateCode == `${day}.${calendar.month}.${calendar.year}`,
'has-data':cycleData[`${day}.${calendar.month}.${calendar.year}`],
'no-data':showDayDataColor(day),
}">
<!-- v-on:click="openDayData(`${day}.${calendar.month}.${calendar.year}`)" -->
<span class="number">{{ day }}</span>
<!-- {{ `${day}.${calendar.month}.${calendar.year}` }} -->
<span class="pill-container" v-for="(entry, dateCode) in getChartData" v-if="dateCode == `${day}.${calendar.month}.${calendar.year}`">
<span
v-for="(dayData, fieldId) in entry"
v-if="showZeroValuesCheck(dayData.value, fieldId)"
class="pill"
:class="[$parent.$parent.getFieldColor(fieldId), {
'did-next':dayData.didNext,
'did-last':dayData.didLast,
'last-high':dayData.lastHigh,
'next-high':dayData.nextHigh,
}]">
<!-- 'zero-day':dayData.value == lowestGraphValue, -->
<!-- <i v-if="dayData.value != 0" :class="`tiny ${$parent.$parent.getFieldColor(fieldId)} ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i> -->
<!-- <span v-else class="icon-spacer"></span>
:style="{height:(Math.round(dayData.value*5)+'px')}"
-->
<span v-if="dayData.value > lowestGraphValue-1" class="big-day">
<i v-if="!hideIcons" :class="`tiny white ${$parent.$parent.getFieldIcon(fieldId)} icon`"></i>
<span v-if="!hideValues">
{{ getDayValue(fieldId, dayData.value) }}
</span>
</span>
</span>
</span>
<!-- <span v-for="fieldId in graph.fieldIds"></span> -->
</div>
</div>
</div>
</template>
<script>
// let chartData = {}
export default {
props: [
'graphOptions', // options associated with this graph
'graph', // options associated with this graph
'userFields', // all field attributes
'tempChartDays', // number of days to display
'cycleData', // all users metric data
'editGraphs', // display additional edit options
// Graph options
'showZeroValues', // Hide graph data with value of zero
'showTextValues', // Show button text or button value
'connectDays', // Calculates next and previous day connections.
'hideValues', // Hide all values on the graph
'hideIcons', // option to hide icons
],
data: function(){
return {
openModel:true,
calendar: {
dateObject: null,
dateCode: null,
monthName: '',
dayName:'',
daysAgo:0,
month: '',
year: '',
days: [],
weekdays: ['S','M','T','W','T','F','S'],
today: 0,
},
chartDateCodes: [], // array of date codes in chart
listDateCodes: [],
dayList: true,
lowestGraphValue: 0,
}
},
mounted(){
this.setupCalendar(new Date())
},
computed: {
getChartData(){
let chartData = {}
let chartValues = []
// iterate every day in month by day code
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
// lookup data for that day
const cycleDayData = this.cycleData[chartDayCode]
// if chart data is set for this day
if( cycleDayData && Object.keys(cycleDayData).length > 0){
chartData[chartDayCode] = {}
// go over each field to be displayed on graph
this.graph.fieldIds.forEach((graphFieldId) => {
if( cycleDayData[graphFieldId] == undefined ){
return
}
// track all chart values
chartValues.push(cycleDayData[graphFieldId])
chartData[chartDayCode][graphFieldId] = {
didLast: false,
lastHigh: false,
didNext: false,
nextHigh: false,
value: cycleDayData[graphFieldId]
}
})
}
})
this.lowestGraphValue = Math.min(...chartValues)
// determine next and previous states for display
this.chartDateCodes.forEach((chartDayCode, codeIndex) => {
if(chartData[chartDayCode] && this.connectDays){
const previousDateCode = this.chartDateCodes[codeIndex-1]
const nextDateCode = this.chartDateCodes[codeIndex+1]
Object.keys(chartData[chartDayCode]).forEach((graphFieldId) => {
const currentValue = chartData[chartDayCode][graphFieldId].value
// check for previous entry
if( chartData[previousDateCode] && chartData[previousDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didLast = true
// set low value flag
const lastHigh = chartData[previousDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].lastHigh = lastHigh && currentValue == 0
}
// check for next entry
if( chartData[nextDateCode] && chartData[nextDateCode][graphFieldId] ){
chartData[chartDayCode][graphFieldId].didNext = true
// set low value flag
const nextHigh = chartData[nextDateCode][graphFieldId].value > 0
chartData[chartDayCode][graphFieldId].nextHigh = nextHigh && currentValue == 0
}
})
}
})
// console.log(chartData)
return chartData
},
},
methods: {
closeModel(){
showZeroValuesCheck(dayValue, fieldId){
// if graph type is boolean or there are two options
let isBooleanField = this.userFields[fieldId].type == 'boolean'
if(this.userFields[fieldId].customOptions){
let options = this.userFields[fieldId].customOptions
isBooleanField = options.split(',').length == 2
}
if(isBooleanField && !this.showZeroValues){
const parsedValue = this.getDayValue(fieldId, dayValue)
if(parsedValue == 'Yes'){
return true
} else {
return false
}
}
return this.showZeroValues || dayValue > this.lowestGraphValue
},
getDayValue(fieldId, value){
if( !this.showTextValues ){
return value
}
let options = 'error, Yes, No'
if(this.userFields[fieldId].customOptions){
options = this.userFields[fieldId].customOptions
}
const values = options.split(',')
const selection = String(values[value]).trim()
return selection
},
displayDayFromCode(dateCode){
const parts = dateCode.split('.')
return `${parts[0]}`
},
showDayDataColor(day){
// Determine if day has any data set
if(day == ''){
return false
}
return !(this.cycleData[`${day}.${this.calendar.month}.${this.calendar.year}`])
},
generateDateCode(date){
const dateSetup = [
date.getDate(), // 1-31 (Day)
date.getMonth()+1, // 0-11 (Month)
date.getFullYear(), // 1888-2022 (Year)
]
return dateSetup.join('.')
},
setupCalendar(date){
// visualize each day change
this.working = true
setTimeout(() => {
this.working = false
}, 500)
if(!date && this.dateObject){
date = this.dateObject
}
if(!date){
date = new Date()
}
this.calendar.dateObject = date
this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------
// setup calendar display
var y = date.getFullYear()
var m = date.getMonth()
var firstDay = new Date(y, m, 1);
var lastDay = new Date(y, m + 1, 0);
function getDaysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
const monthStartDay = firstDay.getDay()
let days = Array(monthStartDay).fill(""); // Pad days to start on correct weekday
for (let i = 0; i < daysInCurrentMonth; i++) {
days.push(i+1)
}
this.calendar.days = days
// set today
this.calendar.today = date.getDate()
this.calendar.month = date.getMonth()+1
// setup date codes for key matching on calendar
this.calendar.days.forEach((day) => {
if( day !== "" ){
let dateDay = new Date(y, m, day);
let dayCode = this.generateDateCode(dateDay)
this.chartDateCodes.push(dayCode)
}
})
// generate past date codes for list
for (let i = 0; i < this.tempChartDays; i++) {
const now = new Date()
const pastDate = now.setDate(now.getDate() - i)
const pastDateObj = new Date(pastDate)
const newCode = this.generateDateCode(pastDateObj)
this.listDateCodes.push(newCode)
}
// return codes.reverse()
/*
October 2022
S M T W T F S
1 2 3 4 5 6
7 8 9
*/
// -------
},
}
}

View File

@@ -30,7 +30,7 @@
@media only screen and (max-width: 740px) {
.modal-content {
width: 100%;
padding-bottom: 55px;
/* padding-bottom: 55px;*/
}
}

View File

@@ -172,8 +172,13 @@
<span class="status-menu" v-on:click=" hash=0; save()">
<span v-if="idleNote" data-position="left center" data-tooltip="Idle: Awaiting Changes">
<i class="vertically flipped grey wifi icon"></i>
</span>
<span v-if="diffsApplied > 0">
+{{ diffsApplied }} Unsaved Changes
<i class="blue wave square icon"></i>
+{{ diffsApplied }}
</span>
<span v-if="usersOnNote > 1" :data-tooltip="`Viewers`" data-position="left center">
@@ -346,6 +351,10 @@
:class="{ 'fade-me-out':sizeDown }"
v-on:click="closeButtonAction()"></div> -->
<div>
</div>
</div>
</template>
@@ -359,6 +368,8 @@
const dmp = new DiffMatchPatch.diff_match_patch()
import SquireButtonFunctions from '@/mixins/SquireButtonFunctions.js'
let rawNoteText = '' // Used for comparing and generating diffs
export default {
name: 'NoteInputPanel',
@@ -390,7 +401,6 @@
created: '',
updated: '',
shareUsername: null,
// diffNoteText: '',
statusText: 'saved',
lastNoteHash: null,
saveDebounce: null, //Prevent save from being called numerous times quickly
@@ -402,13 +412,11 @@
pinned: 0,
archived: 0,
attachmentCount: 0,
attachments: [],
styleObject: { 'noteText':null,'noteBackground':null, 'noteIcon':null, 'iconColor':null }, //Style object. Determines colors and badges
sizeDown: false, //Used to animate close state
//Settings vars
lastVisibilityState: null,
//All the squire settings
editor: null,
usersOnNote: 0,
@@ -424,10 +432,14 @@
//Diff text/sync text variables
diffTextTimeout: null,
diffsApplied: null,
idleNote: true, // If note is idle, get updates from server
idleNoteTimeout: null,
reloadNoteDebounce: null,
//Used to restore caret position
lastRange: null,
startOffset: 0,
childIndex: null,
//Tag Display
allTags: [],
@@ -483,9 +495,12 @@
this.$bus.$off('new_file_upload')
this.destroyAttachmentStyles()
this.destroyWebSockets()
document.removeEventListener('visibilitychange', this.checkForUpdatedNote)
window.removeEventListener('blur', this.windowBlurEvent)
window.removeEventListener('focus', this.windowFocusEvent)
//Obliterate squire instance
this.editor.destroy()
@@ -501,7 +516,9 @@
this.forceShowLoading = true
}, 500)
document.addEventListener('visibilitychange', this.checkForUpdatedNote)
window.addEventListener('blur', this.windowBlurEvent)
window.addEventListener('focus', this.windowFocusEvent)
// this.logNoteInteraction()
//Init squire as early as possible
if(this.editor && this.editor.destroy){
@@ -570,33 +587,31 @@
})
},
initSquire(){
initSquireEvents(){
//Set up squire and load note text
this.setText(this.noteText)
// Use squire box HTML for diff/patch changes
rawNoteText = document.getElementById('squire-id').innerHTML
//focus on open, not on mobile, it causes the keyboard to pop up, thats annoying
if(!this.$store.getters.getIsUserOnMobile){
this.editor.focus()
this.editor.moveCursorToEnd()
}
//Set up websockets after squire is set up
setTimeout(() => {
this.setupWebSockets()
}, 500)
this.editor.addEventListener('cursor', e => {
//Save range to replace cursor if someone else makes an update
this.lastRange = e.range
this.startOffset = parseInt(e.range.startOffset)
return
this.saveCaretPosition(e)
})
//Change button states on editor when element is active
//eg; Bold button turns green when on bold text
this.editor.addEventListener('pathChange', e => this.pathChangeEvent(e))
this.editor.addEventListener('pathChange', e => {
this.pathChangeEvent(e)
this.diffText(e)
})
//Click Event - Open links when clicked in editor or toggle checks
this.editor.addEventListener('click', e => {
@@ -681,10 +696,20 @@
})
})
//Bind event handlers
this.editor.addEventListener('keyup', event => {
this.onKeyup(event)
this.diffText(event)
this.logNoteInteraction()
})
this.editor.addEventListener('focus', e => {
this.logNoteInteraction()
// this.diffText(e)
})
this.editor.addEventListener('blur', e => {
this.idleNote = true
this.diffText(e)
})
// this.editor.addEventListener("dragstart", e => {
@@ -692,12 +717,6 @@
// console.log(e)
// if(){}
// });
//Show and hide additional toolbars
// this.editor.addEventListener('focus', e => {
// })
// this.editor.addEventListener('blur', e => {
// })
},
openEditAttachment(){
@@ -760,13 +779,18 @@
//Setup all responsive vue data
this.setupLoadedNoteData(response)
this.loading = false
this.$nextTick(() => {
//Adjust note title size after load
this.titleResize()
this.initSquire()
this.initSquireEvents()
//Set up websockets after squire is set up
setTimeout(() => {
this.initWebsocketEvents()
this.loading = false
}, 500)
})
})
@@ -786,14 +810,11 @@
this.created = response.data.created
this.updated = response.data.updated
this.lastInteractionTimestamp = +new Date
this.noteTitle = ''
if(response.data.title){
this.noteTitle = response.data.title
}
this.noteTitle = response.data.title || ''
this.noteText = response.data.text
this.lastNoteHash = this.hashString( response.data.text )
// this.diffNoteText = response.data.text
//Setup note tags
this.allTags = response.data.tags ? response.data.tags.split(','):[]
@@ -807,86 +828,186 @@
this.pinned = response.data.pinned
}
this.archived = response.data.archived
// Fetch attachmets if the count changed
if(response.data.attachment_count > 0){
this.getAttachments()
}
this.attachmentCount = response.data.attachment_count
return true
},
generateSelector(el){
if (!(el instanceof Element))
return;
var path = [];
while (el.nodeType === Node.ELEMENT_NODE) {
var selector = el.nodeName.toLowerCase();
if (el.id) {
selector += '#' + el.id;
path.unshift(selector);
break;
} else {
var sib = el, nth = 1;
while (sib = sib.previousElementSibling) {
if (sib.nodeName.toLowerCase() == selector)
nth++;
}
if (nth != 1)
selector += ":nth-of-type("+nth+")";
}
path.unshift(selector);
el = el.parentNode;
}
return path.join(" > ");
},
//Called on squire event for keyup
diffText(event){
// console.log(event.type)
//Diff the changed lines only
const diffEvents = ['keyup','pathChange', 'click']
let oldText = this.noteText
// let newText = this.getText()
let newText = document.getElementById('squire-id').innerHTML
const diff = dmp.diff_main(oldText, newText)
// dmp.diff_cleanupSemantic(diff)
const patch_list = dmp.patch_make(oldText, newText, diff);
const patch_text = dmp.patch_toText(patch_list);
if(patch_text == ''){ return }
//Save computed diff text
this.noteText = newText
let newPatch = {
id: this.rawTextId,
diff: patch_text,
// only process changes on certain events
if( !diffEvents.includes(event?.type) ){
return
}
this.$io.emit('note_diff', newPatch)
},
patchText(incomingPatchs){
return new Promise((resolve, reject) => {
clearTimeout(this.diffTextTimeout)
this.diffTextTimeout = setTimeout(() => {
if(incomingPatchs == null){ return resolve(true) }
if(incomingPatchs.length == 0){ return resolve(true) }
// Current Editor Text
const liveEditorElm = document.getElementById('squire-id')
// let currentText = this.getText()
let currentText = document.getElementById('squire-id').innerHTML
// virtual element for selecting div
let virtualEditorElm = document.createElement('div')
virtualEditorElm.innerHTML = rawNoteText
//Convert text of all new patches into patches array
let patches = []
incomingPatchs.forEach(patch => {
// element at cursor
const elmAtCaret = window.getSelection().getRangeAt(0).startContainer.parentNode
if(patch.time <= this.updated){
return
// Remove beginngin selector from path, make it more generic
const path = this.generateSelector(elmAtCaret).replace('div#squire-id > ','')
let workingPath = ''
// default to entire note text, select down if path
let selectedDivText = virtualEditorElm
let newSelectedDivText = liveEditorElm
if( path != ''){
const pathParts = path.split(' > ')
let testedPathParts = []
let workingPathParts = []
for (var i = 0; i < pathParts.length; i++) {
testedPathParts.push(pathParts[i])
let currentTestPath = testedPathParts.join(' > ')
// console.log('elm test ',i,currentTestPath)
let elmTest = virtualEditorElm.querySelector(currentTestPath)
if(!elmTest){
break
}
workingPathParts.push(pathParts[i])
}
patches.push(...dmp.patch_fromText(patch.diff))
})
if(patches.length == 0){
return resolve(true)
workingPath = workingPathParts.join(' > ')
if(workingPath){
// Select text from virtual editor text
selectedDivText = selectedDivText.querySelector(workingPath)
// select text from current editor text
newSelectedDivText = newSelectedDivText.querySelector(workingPath)
}
}
var results = dmp.patch_apply(patches, currentText);
let newText = results[0]
const oldDivText = selectedDivText.innerHTML
const newDivText = newSelectedDivText.innerHTML
this.noteText = newText
// this.editor.setHTML(newText)
document.getElementById('squire-id').innerHTML = newText
if(oldDivText == newDivText){ return }
return resolve(true)
const diff = dmp.diff_main(oldDivText, newDivText)
const patch_list = dmp.patch_make(oldDivText, newDivText, diff)
const patch_text = dmp.patch_toText(patch_list)
// save raw text for future diffs
rawNoteText = liveEditorElm.innerHTML
let newPatch = {
id: this.rawTextId,
diff: patch_text,
path: path,
// testing metrics
'old text':oldDivText,
'new text':newDivText,
'starting path':path,
'working path':workingPath,
}
// console.log('Sending out patch', newPatch)
this.$io.emit('note_diff', newPatch)
}, 100)
},
patchText(incomingPatchs){
// console.log('incoming patches ', incomingPatchs)
return new Promise((resolve, reject) => {
const editorElement = document.getElementById('squire-id')
// iterate over incoming patches because they apply to specific divs
incomingPatchs.forEach(patch => {
// default to parent element, change to child if set
let editedElement = editorElement
if(patch.path){
editedElement = editorElement.querySelector(patch.path)
}
if( !editedElement ){
editedElement = editorElement
}
// convert patch from text and then apply to selected element
const patches = dmp.patch_fromText(patch.diff)
const patchResults = dmp.patch_apply(patches, editedElement.innerHTML)
// console.log('Patch results')
// console.log([patch.path, editedElement.innerHTML, patchResults[0]])
// patch changed directly into editor
editedElement.innerHTML = patchResults[0]
})
// save editor HTML after change for future comparisons
rawNoteText = editorElement.innerHTML
// update hash on patch
this.lastNoteHash = this.hashString( rawNoteText )
this.$nextTick(() => {
return resolve(true)
})
})
},
onKeyup(event){
this.statusText = 'modified'
// Small debounce on diff generation
clearTimeout(this.diffTextTimeout)
this.diffTextTimeout = setTimeout(() => {
this.diffText()
}, 25)
this.idleNote = false
//Save after x seconds
clearTimeout(this.editDebounce)
this.editDebounce = setTimeout(() => {
this.save()
}, 5 * 1000)
}, 4 * 1000)
//Save after x keystrokes
this.keyPressesCounter = (this.keyPressesCounter + 1)
@@ -919,6 +1040,7 @@
}
//tell websockets to truncate history at this save
this.lastNoteHash = currentHash //Update last saved note hash
this.$io.emit('truncate_diffs_at_save', {'rawTextId':this.rawTextId, 'hash':currentHash })
const postData = {
@@ -939,44 +1061,54 @@
this.modified = true
this.diffsApplied = 0
//Update last saved note hash
this.lastNoteHash = currentHash
return resolve(true)
})
.catch(error => { this.$bus.$emit('notification', 'Failed to Save Note') })
})
},
checkForUpdatedNote(){
loadNoteNextFromServer(){
const now = +new Date
//Only check every 3 seconds
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (2 * 1000)
clearTimeout(this.reloadNoteDebounce)
this.reloadNoteDebounce = setTimeout(() => {
// flash note text to show the update
// this.setText('')
//If user leaves page then returns to page, reload the first batch
if(this.lastVisibilityState == 'hidden' && document.visibilityState == 'visible' && checkForUpdateTimeout){
//Focus Regained on Note, check for update
axios.post('/api/note/get', { 'noteId': this.noteid })
.then(response => {
const serverTextHash = this.hashString( response.data.text )
this.setupLoadedNoteData(response)
if(this.lastNoteHash != serverTextHash){
// console.log('note was changed UPDATE THAT BITCH!!!!')
this.setupLoadedNoteData(response)
//Manually set squire text to show
this.setText(this.noteText)
}
//Manually set squire text to show
this.setText(this.noteText)
})
}
}, 200)
//Keep track of visibility change and last interaction time
this.lastVisibilityState = document.visibilityState
this.lastInteractionTimestamp = +new Date
},
windowFocusEvent(){
//Only check if its been greater than a few seconds
const now = +new Date
const checkForUpdateTimeout = now - this.lastInteractionTimestamp > (3 * 1000)
//If user leaves page then returns to page, reload the first batch
if(checkForUpdateTimeout){
this.loadNoteNextFromServer()
this.lastInteractionTimestamp = now
}
},
windowBlurEvent(){
this.idleNote = true
this.lastInteractionTimestamp = +new Date
},
hashString(inText){
@@ -1030,11 +1162,14 @@
})
},
destroyWebSockets(){
// this.$io.removeListener('past_diffs')
// this.$io.removeListener('update_user_count')
// this.$io.removeListener('incoming_diff')
this.$io.removeListener('past_diffs')
this.$io.removeListener('update_user_count')
this.$io.removeListener('incoming_diff')
this.$io.removeListener('update_note_attachments')
clearTimeout(this.idleNoteTimeout)
},
setupWebSockets(){
initWebsocketEvents(){
//Tell server to push this note into a room
this.$io.emit('join_room', this.rawTextId )
@@ -1050,36 +1185,79 @@
this.diffsApplied = diffSinceLastUpdate.length
// console.log('Got Diffs Total -> ', diffSinceLastUpdate)
}
// console.log(diffSinceLastUpdate)
this.patchText(diffSinceLastUpdate)
.then(() => {
this.restoreCaretPosition()
})
})
this.$io.on('incoming_diff', incomingDiff => {
//Save current caret position
//Find index of child element based on past range
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
const textLines = document.getElementById('squire-id').children
const childIndex = [...textLines].indexOf(element)
this.patchText([incomingDiff])
.then(() => {
if(childIndex == -1){
console.log('Cursor position lost. Div being updated was lost.')
return
}
//Reset caret position
//Find child index of old range and create a new one
let allChildren = document.getElementById('squire-id').children
const newLine = allChildren[childIndex].firstChild
let range = document.createRange()
range.setStart(newLine, this.startOffset)
range.setEnd(newLine, this.startOffset)
this.editor.setSelection(range)
this.restoreCaretPosition()
})
})
this.$io.on('new_note_text_saved', ({noteId, hash}) => {
const sameIdCheck = (this.idleNote && this.noteid == noteId)
const differentHashCheck = (hash != this.lastNoteHash)
// if hashes do not match, reload text from server
if(sameIdCheck && differentHashCheck){
this.loadNoteNextFromServer()
}
})
this.$io.on('update_note_attachments', () => {
this.getAttachments()
})
},
logNoteInteraction(){
this.idleNote = false
clearTimeout(this.idleNoteTimeout)
this.idleNoteTimeout = setTimeout(() => {
this.idleNote = true
}, 5000)
},
saveCaretPosition(event){
//Find index of child element based on past range
const element = window.getSelection().getRangeAt(0).startContainer.parentNode
//Save range to replace cursor if someone else makes an update
this.lastRange = this.generateSelector(element)
this.startOffset = parseInt(event.range.startOffset) || 0
return
},
restoreCaretPosition(){
return new Promise((resolve, reject) => {
// This code is intended to restore caret position to previous location
// when a third party updates the note.
if(!this.lastRange){ return resolve(true) }
const editorElement = document.getElementById('squire-id')
const lastElement = editorElement.querySelector(this.lastRange)
if( !lastElement ){ return resolve(true) }
let range = document.createRange()
range.setStart(lastElement.firstChild, this.startOffset)
range.setEnd(lastElement.firstChild, this.startOffset)
// Set range in editor element
this.editor.setSelection(range)
return resolve(true)
})
},
titleResize(){
//Resize the title field
@@ -1090,6 +1268,88 @@
element.style.height = (element.scrollHeight) +'px'
}
},
destroyAttachmentStyles(){
// Remove attachment preview styles
var head = document.head
var styleElement = document.getElementById('attachmentGeneratedStyles'+this.noteid)
if(styleElement){
head.removeChild(styleElement)
}
},
getAttachments(){
axios.post('/api/attachment/search', {'noteId':this.noteid})
.then( results => {
// generate new style group
var style = document.createElement('style')
style.id = 'attachmentGeneratedStyles'+this.noteid
style.type = 'text/css'
// iterate attachments and build unique style for each
let attachmentStyles = []
results.data.forEach(attachment => {
// thumbnail location
const bgurl = `/api/static/thumb_${attachment.file_location}`
let padding = '2px 0 0'
// increase padding if there is a valid file
if(attachment.file_location){
padding = '13px 0 13px 155px'
}
// unescaped characters will break content attribute
const strippedText = attachment.text
.replace(/'/g, "\\'") //Escape ' s
.replace(/\n/g, '\\A') //Escape new lines
// strip down URL, *= matches anywhere is string
const substringsToRemove = ['https://','http://','www.']
var pattern = new RegExp(substringsToRemove.join('|'), 'g');
const strippedurl = attachment.url
.replace(pattern, '') // remove url protocol
.replace(/&.*/, '') // remove anything after &
const cleanStyle = `
.squire-box a[href*="${strippedurl}" i]::before {
content: '${strippedText}';
display: inline-block;
padding: ${padding};
pointer-events: none;
font-size: 1.3em !important;
width: 100%;
background-position: left center;
background-repeat: no-repeat;
background-size: 140px auto;
background-image: url(${bgurl});
border-bottom: solid 1px var(--main-accent);
margin-bottom: -10px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.squire-box a[href*="${strippedurl}" i] {
font-size: 0.8em;
}
`
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
.replace(/\s+/g, ' ') // remove double spaces
attachmentStyles.push(cleanStyle)
})
// Destroy just before creating new to prevent page jumping
this.destroyAttachmentStyles()
// push new styles into <head>
style.innerHTML = attachmentStyles.join(' ')
document.head.appendChild(style)
})
.catch(error => { console.log(error);this.$bus.$emit('notification', 'Failed to Search Attachments') })
},
}
}
</script>
@@ -1102,6 +1362,11 @@
z-index: 1019;
text-align: right;
}
.status-menu span + span {
border-left: 1px solid #ccc;
margin-left: 4px;
padding-left: 4px;
}
.font-color-bar {
/*width: calc(100% - 8px);*/
@@ -1369,6 +1634,7 @@
}
.edit-button {
padding: 6px 0px 0;
flex-grow: 1;
}
.edit-button > span:not(.ui) {
display: none;

View File

@@ -285,23 +285,28 @@
},
justClosed(){
// Dont do anything when not is closed.
// Its already saved, this will make interface feel snappy
// Scroll note into view
this.$el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center'
})
// this.$el.scrollIntoView({
// behavior: 'smooth',
// block: 'center',
// inline: 'center'
// })
//After scroll, trigger green outline animation
setTimeout(() => {
// this.$bus.$emit('notification','Note Saved')
this.triggerClosedAnimation = true
setTimeout(()=>{
//After 3 seconds, hide it
this.triggerClosedAnimation = false
}, 1500)
// //After scroll, trigger green outline animation
// setTimeout(() => {
}, 500)
// this.triggerClosedAnimation = true
// setTimeout(()=>{
// //After 3 seconds, hide it
// this.triggerClosedAnimation = false
// }, 1500)
// }, 500)
},
},

View File

@@ -1,7 +1,7 @@
<template>
<div>
<div class="button-fix">
<div class="ui right floated basic shrinking icon button" v-on:click="showPasteInputArea">
<i class="paste icon"></i>
<i class="green paste icon"></i>
Paste
</div>
<div class="shade" v-if="showPasteArea" @click.prevent="close">

View File

@@ -172,15 +172,16 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
this.$router.go(-1)
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
setTimeout(()=>{
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
Array.from( container.getElementsByClassName('active') ).forEach(item => {
item.classList.remove('active');
})
},600)
},
deleteCompletedListItems(){
//
@@ -190,53 +191,57 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Close menu if user is on mobile, then sort list
this.$router.go(-1)
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
setTimeout(()=>{
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Create two categories, done and not done list items
let undoneElements = document.createDocumentFragment()
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(!checkedItem){
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
}
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
})
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
//Push checked items and their sub lists to the done set
if(!checkedItem){
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
}
})
}, 600)
},
sortList(){
//
@@ -246,61 +251,65 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Close menu if user is on mobile
this.$router.go(-1)
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
setTimeout(()=>{
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
if(node.nodeName == 'UL'){
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
//Create two categories, done and not done list items
let doneElements = document.createDocumentFragment()
let undoneElements = document.createDocumentFragment()
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
//Go through each item in each list we found
node.childNodes.forEach( (checkListItem, index) => {
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
//Push checked items and their sub lists to the done set
if(checkedItem){
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
//Skip Embedded lists, they are handled with the list item above them. Keep lists with intented items together
if(checkListItem.nodeName == 'UL'){
return
}
} else {
//Check if list item has active class
const checkedItem = checkListItem.classList.contains('active')
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
//Check if the next item is a list, Keep lists with intented items together
let sublist = null
if(node.childNodes[index+1] && node.childNodes[index+1].nodeName == 'UL'){
sublist = node.childNodes[index+1]
}
}
})
//Push checked items and their sub lists to the done set
if(checkedItem){
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
doneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
doneElements.appendChild( sublist.cloneNode(true) )
}
//Close menu if user is on mobile
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
} else {
undoneElements.appendChild( checkListItem.cloneNode(true) )
if(sublist){
undoneElements.appendChild( sublist.cloneNode(true) )
}
}
})
//Remove all HTML from node, push unfinished items, then finished below them
node.innerHTML = null
node.appendChild(undoneElements)
node.appendChild(doneElements)
}
})
},600)
},
calculateMath(){
//
@@ -310,6 +319,9 @@ const SquireButtonFunctions = {
//Fetch the container
let container = document.getElementById('squire-id')
//Close menu if user is on mobile, then sort list
this.$router.go(-1)
// simple function that trys to evaluate javascript
const shittyMath = (string) => {
//Remove all chars but math chars
@@ -322,38 +334,39 @@ const SquireButtonFunctions = {
}
}
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
setTimeout(()=>{
const line = node.innerText.trim()
//Go through each item, on first level, look for Unordered Lists
container.childNodes.forEach( (node) => {
// = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
const line = node.innerText.trim()
//Pull out everything before the formula and try to evaluate it
const formula = line.split('=').shift()
const output = shittyMath(formula)
// = sign exists and its the last character in the string
if(line.indexOf('=') != -1 && (line.length-1) == line.indexOf('=')){
//If its a number and didn't throw an error, update the line
if(!isNaN(output) && output != null){
//Pull out everything before the formula and try to evaluate it
const formula = line.split('=').shift()
const output = shittyMath(formula)
//Since there is HTML in the line, splice in the number after the = sign
let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//If its a number and didn't throw an error, update the line
if(!isNaN(output) && output != null){
//Slam in that new HTML with the output
node.innerHTML = newLine
//Since there is HTML in the line, splice in the number after the = sign
let equalLocation = node.innerHTML.indexOf('=')
let newLine = node.innerHTML.slice(0, equalLocation+1).trim()
newLine += ` ${output}`
newLine += node.innerHTML.slice(equalLocation+1).trim()
//Slam in that new HTML with the output
node.innerHTML = newLine
}
}
}
})
})
},600)
//Close menu if user is on mobile, then sort list
if(this.$store.getters.getIsUserOnMobile){
this.$router.go(-1)
}
},
setText(inText){

View File

@@ -8,6 +8,13 @@
<div class="content">
Files
<div class="sub header">Uploaded Files and Websites from notes.</div>
<div class="sub header">
<i class="green angle double up icon icon"></i>
<router-link
to="/bookmarklet">
Push any website to solid scribe
</router-link>
</div>
</div>
</h2>
@@ -125,6 +132,11 @@
//Load more attachments on scroll
window.addEventListener('scroll', this.onScroll)
this.$io.on('update_note_attachments', () => {
this.reset()
this.searchAttachments()
})
//Mount notes on load if note ID is set
this.searchAttachments()
},
@@ -132,6 +144,8 @@
//Remove scroll event on destroy
window.removeEventListener('scroll', this.onScroll)
this.$io.removeListener('update_note_attachments')
},
watch:{
$route (to, from){

View File

@@ -0,0 +1,66 @@
<template>
<div class="text-container squire-box">
<h2 class="ui header">
<i class="green angle double up icon icon"></i>
<div class="content">
Push URL to Solid Scribe - Bookmarklet
<div class="sub header">Push any website to your file list.</div>
</div>
</h2>
<p>A bookmarklet is a small piece of code that can be run from a bookmark.</p>
<p>Use the bookmarklet below to push URLs of website to solid scribe for later</p>
<p>The bookmarklet works in a secure way and won't leak any data.</p>
<p>To install the bookmarklet, all you need to do is drag it to your bookmarks bar.</p>
<h2>
Drag the link below to your bookmarks.
</h2>
<h3>
<a :href="`${(bookmarkletscript)}`" class="ui huge text">Push to SolidScribe</a>
</h3>
</div>
</template>
<script>
import axios from 'axios'
export default {
components: {
},
data: function(){
return {
loading: true,
bookmarkletscript:'',
}
},
beforeCreate: function(){
// Perform Login check
this.$parent.loginGateway()
},
mounted: function(){
this.getBookmarklet()
},
beforeDestroy(){
},
methods: {
getBookmarklet(){
this.loading = true
axios.post('/api/attachment/getbookmarklet')
.then( results => {
this.bookmarkletscript = results.data
})
.catch(error => { this.$bus.$emit('notification', 'Failed to get bookmarklet') })
},
}
}
</script>

View File

@@ -1,4 +1,4 @@
<style>
<style scoped>
div.no-padding {
padding: 10px 0 40px !important;
box-sizing: border-box;
@@ -233,16 +233,26 @@
}
}
.an-graph {
background: #fefefe82;
.days-ago-display {
font-size: 0.7em;
}
.input-grid {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
}
.bottom.aligned.row {
flex: 1 1 auto;
}
</style>
<template>
<div class="squire-box no-padding" ref="scrollcontainer">
<!-- intro modal -->
<modal v-if="!noteId" :click-out-close="false">
<div class="ui segment">
<div class="ui center aligned middle aligned grid">
@@ -273,10 +283,10 @@
</div>
<!-- section 1 entry -->
<section class="swipe-section">
<section class="swipe-section" v-if="true">
<!-- <delete-button v-if="noteId > 0" class="ui small button" :note-id="noteId" /> -->
<div class="ui small centered dividing header">
Metric Tracking Bet
Metric Tracking
<span class="sub header"><i class="small lock icon"></i>All data Encrypted. Only accessible by you.</span>
</div>
@@ -314,10 +324,18 @@
<!-- data input -->
<div class="ui basic segment">
<div class="ui very compact grid">
<div class="ui twelve wide middle aligned column">
<div class="four wide column"></div>
<div class="ui eight wide middle aligned center aligned column">
<div class="ui header">
Entry for {{ calendar.monthName }},
<span :class="{'loading-day-title':loadingDay}">{{ calendar.today }}</span>
<span :class="{'loading-day-title':loadingDay}">
{{ calendar.monthName }}
{{ calendar.today }}, {{ calendar.dayName }}
<span class="days-ago-display">
<!-- <span v-if="calendar.daysAgo == 0">Today</span> -->
<span v-if="calendar.daysAgo == 1"><br>{{ calendar.daysAgo }} day ago</span>
<span v-if="calendar.daysAgo > 1"><br>{{ calendar.daysAgo }} days ago</span>
</span>
</span>
</div>
</div>
<div class="ui four wide right aligned middle aligned column">
@@ -339,77 +357,89 @@
<div class="ui form" :class="{'loading-day':loadingDay}">
<draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable">
<div v-for="field in fields" :key="field"
:class="userFields[field]?.width ? userFields[field]?.width+' wide column':'eight wide column'">
<!-- field label display -->
<div class="ui very compact grid">
<div class="ui sixteen wide center aligned column">
<i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
<b>{{ userFields[field]?.label }}</b>
<span>{{ field }}</span>
</div>
</div>
:class="userFields[field]?.width ? userFields[field]?.width+' wide stretched column':'eight wide stretched column'">
<!-- float -->
<div v-if="userFields[field]?.type == 'float'" class="ui fluid input">
<input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]">
</div>
<!-- range -->
<div v-if="userFields[field]?.type == 'shortRange'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">No</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypes[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
<div class="ui very compact grid input-grid">
<!-- field label display -->
<div class="row">
<div class="ui sixteen wide column">
<b><i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
{{ userFields[field]?.label }}</b>
<!-- <span>{{ field }}</span> -->
</div>
</div>
</div>
<!-- input display -->
<div class="row">
<div class="sixteen wide column">
<div class="ui very compact grid">
<div class="ui six wide column">
<span v-on:click="editField(field)">
<i class="clickable grey edit outline icon"></i>
</span>
<!-- float -->
<div v-if="userFields[field]?.type == 'float'" class="ui fluid input">
<input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]">
</div>
<!-- range -->
<div v-if="userFields[field]?.type == 'shortRange'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div class="option-buttons">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui compact button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui compact button">No</div>
</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypesDef[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
</div>
</div>
</div>
</div>
</div>
<div class="four wide center aligned column draggable">
<span>
<i class="grey grip lines icon"></i>
</span>
</div>
<div class="ui six wide right aligned column">
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')">
<i class="grey clickable reply icon"></i>
</span>
<div class="bottom aligned row">
<div class="ui six wide column">
<span v-on:click="editField(field)">
<i class="clickable grey edit outline icon"></i>
</span>
</div>
<div class="four wide center aligned column draggable">
<span>
<i class="grey grip lines icon"></i>
</span>
</div>
<div class="ui six wide right aligned column">
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')">
<i class="grey clickable reply icon"></i>
</span>
</div>
</div>
</div>
</div>
@@ -420,7 +450,7 @@
</section>
<!-- section 2 analysis -->
<section class="swipe-section">
<section class="swipe-section" v-if="!loading">
<div class="ui small centered dividing header">
Review Data
@@ -445,7 +475,7 @@
</div>
<!-- calendar -->
<div class="calendar">
<div class="calendar" v-if="false">
<div v-for="day in calendar.weekdays" class="day">
{{ day }}
@@ -476,7 +506,7 @@
</div>
</div>
<div class="ui segment">
<div class="ui segment" v-if="false">
<a class="ui clickable" v-on:click="toggleFolded('key')">
<i class="tiny circular blue clickable plus icon"></i>
Calendar Explanation
@@ -558,31 +588,40 @@
<!-- Temp graph -->
<div class="ui basic segment" >
<div class="ui dividing header">
Chart data for the last {{ tempChartDays }} days
Chart data for the last {{ tempChartDays }} entries
</div>
<div class="ui tiny compact fluid buttons">
<div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; graphCurrentData()" class="ui button">ALL</div>
<div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; graphCurrentData()" class="ui button">90</div>
<div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; graphCurrentData()" class="ui button">60</div>
<div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; graphCurrentData()" class="ui button">30</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; graphCurrentData()" class="ui button">7</div>
<div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; " class="ui button">ALL</div>
<div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; " class="ui button">90</div>
<div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; " class="ui button">60</div>
<div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; " class="ui button">30</div>
<div :class="{'green':(tempChartDays == 15)}" v-on:click="tempChartDays = 15; " class="ui button">15</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; " class="ui button">7</div>
</div>
<!-- <div id="graphdiv" style="width: 100%; min-height: 320px;"></div> -->
<div class="ui divider"></div>
<div class="ui divider"></div>
<div v-for="(graph, index) in graphs" class="an-graph">
<div class="ui small dividing header">
{{ graph?.title }}
</div>
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
<br>
<MetricGraphs
:key="'graph-updates-'+graphUpdates"
:tempChartDays="tempChartDays"
:fields="fields"
:userFields="userFields"
:graphs="graphs"
:cycleData="cycleData"
:calendar="calendar"
@saveGraphs="saveGraphs"
@toggleEditGraphs="toggleEditGraphs"
:editGraphs="editGraphs"
/>
<div class="ui very padded basic segment">
</div>
</div>
<!-- notes -->
<div class="ui basic segment">
<div class="ui basic segment" v-if="false">
<div class="ui clickable" v-on:click="toggleFolded('notes')">
<i class="tiny circular blue clickable plus icon"></i>
Additional Notes
@@ -780,21 +819,21 @@
</div>
<div v-for="(entry, key) in fieldDefinition" class="row" v-if="!['id'].includes(key)">
<div v-if="fieldDefinitionOptions[key]" class="sixteen wide column">
<div v-if="fieldDefinitionOptionsDef[key]" class="sixteen wide column">
{{ mapFormTerm(key) }}
<div v-if="fieldDefinitionOptions[key].type == 'text'" class="ui fluid input">
<div v-if="fieldDefinitionOptionsDef[key].type == 'text'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div>
<div v-if="fieldDefinitionOptions[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input">
<div v-if="fieldDefinitionOptionsDef[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div>
<div v-if="fieldDefinitionOptions[key].type == 'option'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'option'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}"
class="ui button">
@@ -802,8 +841,8 @@
</div>
</div>
<div v-if="fieldDefinitionOptions[key].type == 'icons'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'icons'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}"
class="ui icon button">
@@ -812,8 +851,8 @@
</div>
<!-- :class="{'green':}" -->
<div v-if="fieldDefinitionOptions[key].type == 'color'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'color'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="`ui ${option} icon button`">
<i v-if="editFieldObject[key] == option" class="white check icon"></i>
@@ -865,10 +904,6 @@
import axios from 'axios'
import draggable from 'vuedraggable'
import { Chart } from 'chart.js/auto'
var BASAL_TEMP = 'BT'
export default {
name: 'MetricTracking',
components: {
@@ -876,6 +911,7 @@
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
'modal': require('@/components/ModalComponent.vue').default,
draggable,
'MetricGraphs':require('@/components/Metrictracking/MetricGraphsComponent.vue').default,
},
data () {
return {
@@ -886,15 +922,8 @@
showNotes: false,
fields:[], // Array of field IDs
userFields:{}, // Objects of field definitions
graphs:[
// types [null, 'lastDone', 'pillCalendar']
{type:'pillCalendar', title:'Pill Cal', fieldIds:['JX3QD','K3KII']},
{type:'lastDone', title:'Last Done', fieldIds:['JX3QD','K3KII']},
{title:'Basal Temp', fieldIds:['BT']},
{title:'Basal Temp - Cervical Fluid', fieldIds:['BT','CM']},
{title:'Test Graph', fieldIds:['40DA6','MI9B9']},
],
fieldTypes:{
graphs:[],
fieldTypesDef:{
'float':'Precise Number',
'shortRange':'Options 1-5',
'longRange':'Options 1-10',
@@ -922,7 +951,7 @@
fieldDefinition:{
'id':'','type':'','label':'','icon':'','color':'','width':'','customOptions':''
},
fieldDefinitionOptions:{
fieldDefinitionOptionsDef:{
label:{'type':'text'},
customOptions:{'type':'commatextoptions'},
icon:{'type':'icons', 'options':['heart','smile','cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']},
@@ -931,15 +960,18 @@
},
cycleData: {},
totalEntries: 0,
openDay: {},
openDay: {}, // current day values, updates into cycleData when saved
loadingDayTimeout: null,
loadingDay: false,
saveDataDebounce:null,
loading: true,
saving: 0, // 0 blank, 1 modified, 2 saving, 3 saved
calendar: {
dateObject: null,
dateCode: null,
monthName: '',
dayName:'',
daysAgo:0,
month: '',
year: '',
days: [],
@@ -952,6 +984,8 @@
editFieldId:'',
editFieldObject:{},
appDataImport:'',
graphUpdates:0,
editGraphs: false,
}
},
beforeCreate() {
@@ -972,11 +1006,6 @@
location.reload();
})
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
// setup date to today
this.setupCalendar()
@@ -1035,6 +1064,19 @@
}
},
methods: {
saveGraphs(newGraphData){
this.graphs = newGraphData
this.saveCycleData()
// re-render graph data on update
this.$nextTick(() => {
this.graphUpdates++
})
},
toggleEditGraphs(){
this.editGraphs = !this.editGraphs
},
getFieldColor(field){
let color = null
@@ -1134,14 +1176,14 @@
validateCustomFieldsForm(){
const checks = [] //check
const checkFields = Object.keys(this.fieldDefinitionOptions)
const checkFields = Object.keys(this.fieldDefinitionOptionsDef)
checkFields.forEach(row => {
// console.log(this.editFieldObject[row])
// don't worry about optional fields
if(this.fieldDefinitionOptions[row].optional){
if(this.fieldDefinitionOptionsDef[row].optional){
checks.push(true)
return
}
@@ -1323,98 +1365,6 @@
this.fields.push(fieldId)
this.saveCycleData()
},
graphCurrentData(){
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
const sortableDate = (dateCode) => {
return parseInt(
dateCode
.split('.')
.map(i => String(i).padStart(2, '0'))
.reverse()
.join('')
)
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys.sort((a,b) => {
a = sortableDate(a)
b = sortableDate(b)
return b - a
})
dataKeys = dataKeys.splice(0, this.tempChartDays)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
graphLabels.push(graphLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if(fieldId == 'CM'){
currentValue = parseFloat('97.'+currentValue)
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
saveField(fieldId, value, optionalTimeout){
// Dont save value if it hasn't changed
@@ -1472,8 +1422,6 @@
this.cycleData[this.calendar.dateCode] = cleanDayData
}
this.graphCurrentData()
this.saveCycleData()
},
@@ -1499,6 +1447,7 @@
axios.post('/api/metric-tracking/get')
.then(({ data }) => {
this.loading = false
this.setApplicationStateJson(data)
})
@@ -1532,27 +1481,49 @@
this.cycleData = json?.cycleData || this.cycleData
this.fields = [...new Set(json?.fields)] || this.fields
this.userFields = json?.userFields || this.userFields
// this.graphs = json?.graphs || this.graphs
// console.log(this.fields)
this.graphs = json?.graphs || this.graphs
this.$nextTick(() => {
this.getApplicationStateJson()
this.totalEntries = Object.keys(this.cycleData).length
this.setupFields()
this.openDayData(this.calendar.dateCode)
this.graphCurrentData()
this.generateTonsOfRandomData()
})
},
getApplicationStateJson(){
// convert date code into sortable int
const sortableDate = (code) => {
code = code
.split('.')
.reverse()
.map(i => String(i).padStart(2, '0'))
.join('')
return parseInt(code)
}
// Sort cycle data, newest first
let sortedData = Object.keys(this.cycleData)
.sort((a,b) => {
return sortableDate(b) - sortableDate(a)
})
// setup new object with sorted data
let sortedCycleData = sortedData.reduce((result, key) => {
result[key] = this.cycleData[key]
return result
},{})
return JSON.stringify({
fields: this.fields,
cycleData: this.cycleData,
cycleData: sortedCycleData,
userFields: this.userFields,
// graphs: this.graphs,
graphs: this.graphs,
})
},
saveCycleData(){
@@ -1624,6 +1595,7 @@
},
setupCalendar(date){
// visualize each day change
this.working = true
setTimeout(() => {
this.working = false
@@ -1640,6 +1612,13 @@
this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------
// setup calendar display
@@ -1656,6 +1635,7 @@
const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
@@ -1704,7 +1684,6 @@
workingDate.setDate(workingDate.getDate()-1)
}
this.graphCurrentData(5000)
},
}
}

View File

@@ -36,10 +36,6 @@
/>
<paste-button />
<span class="ui grey text text-fix">
Active Sessions {{ $store.getters.getActiveSessions }}
</span>
</div>
@@ -571,7 +567,7 @@
// @TODO Don't even trigger this if the note wasn't changed
updateSingleNote(noteId, focuseAndAnimate = true){
console.log('updating single note', noteId)
// console.log('updating single note', noteId)
noteId = parseInt(noteId)

View File

@@ -13,6 +13,7 @@ const NotesPage = () => import(/* webpackChunkName: "NotesPage" */ '@/pages/Note
const QuickPage = () => import(/* webpackChunkName: "QuickPage" */ '@/pages/QuickPage')
const AttachmentsPage = () => import(/* webpackChunkName: "AttachmentsPage" */ '@/pages/AttachmentsPage')
const OverviewPage = () => import(/* webpackChunkName: "OverviewPage" */ '@/pages/OverviewPage')
const BookmarkletPage = () => import(/* webpackChunkName: "BookmarkletPage" */ '@/pages/BookmarkletPage')
const NotFoundPage = () => import(/* webpackChunkName: "404Page" */ '@/pages/NotFoundPage')
Vue.use(Router)
@@ -67,6 +68,12 @@ export default new Router({
meta: {title:'Terms'},
component: TermsPage
},
{
path: '/bookmarklet',
name: 'Bookmarklet',
meta: {title:'Bookmarklet'},
component: BookmarkletPage
},
{
path: '/settings',
name: 'Settings',

View File

@@ -44,7 +44,7 @@ export default new Vuex.Store({
'menu-text': '#5e6268',
},
'black':{
'body_bg_color': 'linear-gradient(135deg, rgba(0,0,0,1) 0%, rgba(23,12,46,1) 100%)',
'body_bg_color': 'rgb(12 4 30)',
//'#0f0f0f',//'#000',
'small_element_bg_color': '#000',
'text_color': '#FFF',

11
jest.config.js Normal file
View File

@@ -0,0 +1,11 @@
const path = '../../'
const prefix = '/$1'
module.exports = {
moduleNameMapper: {
"@root/(.*)": ".",
"@models/(.*)": path+"server/models"+prefix,
"@routes/(.*)": path+"server/routes"+prefix,
"@helpers/(.*)": path+"server/helpers"+prefix,
"@config/(.*)": path+"server/config"+prefix,
}
}

6486
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "personal-internet",
"version": "1.0.0",
"description": "Personal or Private net",
"description": "Encrypted note taking applications",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "jest"
},
"author": "Max",
"license": "ISC",
@@ -33,5 +33,8 @@
"@routes": "server/routes",
"@helpers": "server/helpers",
"@config": "server/config"
},
"devDependencies": {
"jest": "^29.7.0"
}
}

View File

@@ -1,5 +1,7 @@
//Import mysql2 package
const mysql = require('mysql2');
const os = require('os') //Used to get path of home directory
const result = require('dotenv').config({ path:(os.homedir()+'/.env') })
// Create the connection pool.
const pool = mysql.createPool({

View File

@@ -72,6 +72,8 @@ CryptoString.createSalt = () => {
return crypto.randomBytes(SALT_BYTE_SIZE).toString('base64')
}
// Creates a small random salt
CryptoString.createSmallSalt = () => {
return crypto.randomBytes(20).toString('base64')

View File

@@ -6,7 +6,7 @@ let SiteScrape = module.exports = {}
const removeWhitespace = /\s+/g
const commonWords = ['share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
const commonWords = ['just','start','what','these','how', 'was', 'being','can','way','share','facebook','twitter','reddit','be','have','do','say','get','make','go','know','take','see','come','think','look','want',
'give','use','find','tell','ask','work','seem','feel','try','leave','call','good','new','first','last','long','great','little','own','other','old',
'right','big','high','different','small','large','next','early','young','important','few','public','bad','same','able','to','of','in','for','on',
'with','at','by','from','up','about','into','over','after','the','and','a','that','I','it','not','he','as','you','this','but','his','they','her',
@@ -162,19 +162,28 @@ SiteScrape.getKeywords = ($) => {
majorContent += $('[class*=content]').text()
.replace(removeWhitespace, " ") //Remove all whitespace
.replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,3000) //Limit to 3000 characters
// .replace(/\W\s/g, '') //Remove all non alphanumeric characters
.substring(0,6000) //Limit to 6000 characters
.toLowerCase()
.replace(/[^A-Za-z0-9- ]/g, '');
console.log(majorContent)
//Count frequency of each word in scraped text
let frequency = {}
majorContent.split(' ').forEach(word => {
if(commonWords.includes(word)){
return //Exclude certain words
// Exclude short or common words
if(commonWords.includes(word) || word.length <= 2){
return
}
if(!frequency[word]){
frequency[word] = 0
}
// Skip some plurals
if(frequency[word+'s'] || frequency[word+'es']){
return
}
frequency[word]++
})
@@ -192,7 +201,7 @@ SiteScrape.getKeywords = ($) => {
});
let finalWords = []
for(let i=0; i<5; i++){
for(let i=0; i<6; i++){
if(sortable[i] && sortable[i][0]){
finalWords.push(sortable[i][0])
}

View File

@@ -20,6 +20,8 @@ const helmet = require('helmet')
const express = require('express')
const app = express()
app.use( helmet() )
// allow for the parsing of url encoded forms
app.use(express.urlencoded({ extended: true }));
//
@@ -114,29 +116,12 @@ io.on('connection', function(socket){
//Emit all sorted diffs to user
socket.emit('past_diffs', noteDiffs[rawTextId])
} else {
socket.emit('past_diffs', null)
}
const usersInRoom = io.sockets.adapter.rooms[rawTextId]
if(usersInRoom){
//Update users in room count
io.to(rawTextId).emit('update_user_count', usersInRoom.length)
//Debugging text - prints out notes in limbo
let noteDiffKeys = Object.keys(noteDiffs)
let totalDiffs = 0
noteDiffKeys.forEach(diffSetKey => {
if(noteDiffs[diffSetKey]){
totalDiffs += noteDiffs[diffSetKey].length
}
})
//Debugging Text
if(noteDiffKeys.length > 0){
console.log('Total notes in limbo -> ', noteDiffKeys.length)
console.log('Total Diffs for all notes -> ', totalDiffs)
}
}
})
@@ -162,31 +147,13 @@ io.on('connection', function(socket){
noteDiffs[noteId].push(data)
//Remove duplicate diffs if they exist
for (var i = noteDiffs[noteId].length - 1; i >= 0; i--) {
let pastDiff = noteDiffs[noteId][i]
for (var j = noteDiffs[noteId].length - 1; j >= 0; j--) {
let currentDiff = noteDiffs[noteId][j]
if(i == j){
continue
}
if(currentDiff.diff == pastDiff.diff || currentDiff.time == pastDiff.time){
console.log('Removing Duplicate')
noteDiffs[noteId].splice(i,1)
}
}
}
//Each user joins a room when they open the app.
// Go over each user in this note-room
io.in(noteId).clients((error, clients) => {
if (error) throw error;
//Go through each client in note room and send them the diff
//Go through each client in note-room and send them the diff
clients.forEach(socketId => {
// only send off diff if user
if(socketId != socket.id){
io.to(socketId).emit('incoming_diff', data)
}
@@ -213,7 +180,6 @@ io.on('connection', function(socket){
}
}
noteDiffs[checkpoint.rawTextId] = diffSet.slice(0, sliceTo)
if(noteDiffs[checkpoint.rawTextId].length == 0){
@@ -235,7 +201,7 @@ io.on('connection', function(socket){
http.listen(ports.socketIo, function(){
console.log(`Socke.io: Listening on port ${ports.socketIo}!`)
console.log(`Socke.io: Listening on port ${ports.socketIo}`)
});
//Enable json body parsing in requests. Allows me to post data in ajax calls
@@ -276,17 +242,21 @@ app.use(function(req, res, next){
// Test Area
const printResults = true
let UserTest = require('@models/User')
let NoteTest = require('@models/Note')
let AuthTest = require('@helpers/Auth')
Auth.test()
UserTest.keyPairTest('genMan30', '1', printResults)
.then( ({testUserId, masterKey}) => NoteTest.test(testUserId, masterKey, printResults))
.then( message => {
if(printResults) console.log(message)
Auth.testTwoFactor()
})
// const printResults = true
// let UserTest = require('@models/User')
// let NoteTest = require('@models/Note')
// let AuthTest = require('@helpers/Auth')
// Auth.test()
// UserTest.keyPairTest('genMan30', '1', printResults)
// .then( ({testUserId, masterKey}) =>
// NoteTest.test(testUserId, masterKey, printResults))
// .then( message => {
// if(printResults) console.log(message)
// Auth.testTwoFactor()
// })
// .catch((error) => {
// console.log(error)
// })
//Test
app.get('/api', (req, res) => res.send('Solidscribe /API is up and running'))

View File

@@ -1,6 +1,7 @@
let db = require('@config/database')
let SiteScrape = require('@helpers/SiteScrape')
const cs = require('@helpers/CryptoString')
let Attachment = module.exports = {}
@@ -47,13 +48,15 @@ Attachment.textSearch = (userId, searchTerm) => {
}
Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeShared) => {
console.log([userId, noteId, attachmentType, offset, setSize, includeShared])
return new Promise((resolve, reject) => {
let params = [userId]
let query = `
SELECT attachment.*, note.share_user_id FROM attachment
JOIN note ON (attachment.note_id = note.id)
WHERE attachment.user_id = ? AND visible = 1 `
LEFT JOIN note ON (attachment.note_id = note.id)
WHERE attachment.user_id = ? AND visible = 1
`
if(noteId && noteId > 0){
//
@@ -76,6 +79,11 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
query += `AND note.archived = ${ attachmentType == 'archived' ? '1':'0' } `
query += `AND note.trashed = ${ attachmentType == 'trashed' ? '1':'0' } `
if(!attachmentType){
// Null note ID means it was pushed by bookmarklet
query += 'OR attachment.note_id IS NULL '
}
}
@@ -102,18 +110,6 @@ Attachment.search = (userId, noteId, attachmentType, offset, setSize, includeSha
})
}
//Returns all attachments
Attachment.forNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
.query(`SELECT * FROM attachment WHERE user_id = ? AND note_id = ? AND visible = 1 ORDER BY last_indexed DESC;`, [userId, noteId])
.then((rows, fields) => {
resolve(rows[0]) //Return all attachments found by query
})
.catch(console.log)
})
}
Attachment.urlForNote = (userId, noteId) => {
return new Promise((resolve, reject) => {
db.promise()
@@ -189,6 +185,7 @@ Attachment.delete = (userId, attachmentId, urlDelete = false) => {
.catch(console.log)
}
})
.catch(console.log)
})
}
@@ -305,9 +302,13 @@ Attachment.scanTextForWebsites = (io, userId, noteId, noteText) => {
//Once everything is done being scraped, emit new attachment events
SocketIo.to(userId).emit('update_counts')
// Tell user to update attachments with scraped text
SocketIo.to(userId).emit('update_note_attachments')
solrAttachmentText += freshlyScrapedText
resolve(solrAttachmentText)
})
.catch(console.log)
})
})
}
@@ -335,9 +336,13 @@ Attachment.scrapeUrlsCreateAttachments = (userId, noteId, foundUrls) => {
//All URLs have been scraped, return data
if(processedCount == foundUrls.length){
resolve(scrapedText)
console.log('All urls scraped')
return resolve(scrapedText)
}
})
.catch(error => {
console.log('Site Scrape error', error)
})
})
})
}
@@ -347,8 +352,8 @@ Attachment.downloadFileFromUrl = (url) => {
return new Promise((resolve, reject) => {
if(url == null || url == undefined || url == ''){
resolve(null)
if(!url){
return resolve(null)
}
const random = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
@@ -356,8 +361,7 @@ Attachment.downloadFileFromUrl = (url) => {
let fileName = random+'_scrape'
let thumbPath = 'thumb_'+fileName
console.log('Scraping image url')
console.log(url)
console.log('Scraping image url', url)
console.log('Getting ready to scrape ', url)
@@ -395,7 +399,7 @@ Attachment.downloadFileFromUrl = (url) => {
Attachment.processUrl = (userId, noteId, url) => {
const scrapeTime = 20*1000;
const scrapeTime = 5*1000;
return new Promise((resolve, reject) => {
@@ -450,9 +454,10 @@ Attachment.processUrl = (userId, noteId, url) => {
var desiredSearchText = ''
desiredSearchText += pageTitle
if(keywords){
desiredSearchText += "\n" + keywords
desiredSearchText += "\n " + keywords
}
console.log('Results from site scrape-------------')
console.log({
pageTitle,
hostname,
@@ -502,40 +507,142 @@ Attachment.processUrl = (userId, noteId, url) => {
})
.catch(error => {
// console.log('Scrape pooped out')
// console.log('Issue with scrape')
console.log(error)
// resolve('')
console.log('Scrape pooped out')
console.log('Issue with scrape', error.statusCode)
clearTimeout(requestTimeout)
return resolve('No site text')
})
requestTimeout = setTimeout( () => {
console.log('Cancel the request, its taking to long.')
request.cancel()
desiredSearchText = 'No Description for -> '+url
created = Math.round((+new Date)/1000)
db.promise()
.query(`UPDATE attachment SET
text = ?,
last_indexed = ?,
WHERE id = ?
`, [desiredSearchText, created, insertedId])
.then((rows, fields) => {
resolve(desiredSearchText) //Return found text
})
.catch(console.log)
//Create attachment in DB with scrape text and provided data
// db.promise()
// .query(`INSERT INTO attachment
// (note_id, user_id, attachment_type, text, url, last_indexed)
// VALUES (?, ?, ?, ?, ?, ?)`, [noteId, userId, 1, desiredSearchText, url, created])
// .then((rows, fields) => {
// resolve(desiredSearchText) //Return found text
// })
// .catch(console.log)
return resolve('Request Timeout')
}, scrapeTime )
})
}
Attachment.generatePushKey = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query("SELECT pushkey FROM user WHERE id = ? LIMIT 1", [userId])
.then((rows, fields) => {
const pushKey = rows[0][0].pushkey
// push key exists
if(pushKey && pushKey.length > 0){
return resolve(pushKey)
} else {
// generate and save a new key
const newPushKey = cs.createSmallSalt()
db.promise()
.query('UPDATE user SET pushkey = ? WHERE id = ? LIMIT 1', [newPushKey,userId])
.then((rows, fields) => {
return resolve(newPushKey)
})
}
})
})
}
Attachment.deletePushKey = (userId) => {
return new Promise((resolve, reject) => {
db.promise()
.query('UPDATE user SET pushkey = null WHERE id = ? LIMIT 1', [userId])
.then((rows, fields) => {
return resolve(rows[0].affectedRows == 1)
})
})
}
Attachment.getPushkeyBookmarklet = (userId) => {
return new Promise((resolve, reject) => {
Attachment.generatePushKey(userId)
.then( pushKey => {
let bookmarklet = Attachment.generateBookmarkletText(pushKey)
return resolve(bookmarklet)
})
})
}
Attachment.pushUrl = (pushkey,url) => {
return new Promise((resolve, reject) => {
let userId = null
pushkey = pushkey.replace(/ /g, '+')
db.promise()
.query("SELECT id FROM user WHERE pushkey = ? LIMIT 1", [pushkey])
.then((rows, fields) => {
if(rows[0].length == 0){
return resolve(true)
}
userId = rows[0][0].id
return Attachment.scrapeUrlsCreateAttachments(userId, null, [url])
})
.then(() => {
if(typeof SocketIo != 'undefined'){
//Once everything is done being scraped, emit new attachment events
SocketIo.to(userId).emit('update_counts')
// Tell user to update attachments with scraped text
SocketIo.to(userId).emit('update_note_attachments')
}
return resolve(true)
})
.catch(console.log)
})
}
Attachment.generateBookmarkletText = (pushKey) => {
const endpoint = '/api/public/pushmebaby'
let url = 'https://www.solidscribe.com' + endpoint
if(process.env.NODE_ENV === 'development'){
// url = 'https://192.168.1.164' + endpoint
}
// Terminate each line with a semi-colon, super important, since spaces are removed.
// document.getElementById(id).remove();
url += '?pushkey='+encodeURIComponent(pushKey)
const bookmarkletV3 = `
javascript: (() => {
var p = encodeURIComponent(window.location.href);
var n = "`+url+`&url="+p;
window.open(n, '_blank', 'noopener=noopener');
window.focus();
var k = document.createElement("div");
k.setAttribute("style", "position:fixed;right:10px;top:10px;z-index:222222;border-radius:4px;font-size:1.3em;padding:20px 15px;background: #8f51be;color:white;");
k.innerHTML = "Posted URL to your Solid Scribe account";
document.body.appendChild(k);
setTimeout(()=>{
k.remove();
},5000);
})();
`
return bookmarkletV3
.replace(/\t|\r|\n/gm, "") // Remove tabs, new lines, returns
.replace(/\s+/g, ' ') // remove double spaces
.trim()
}

View File

@@ -17,6 +17,7 @@ const fs = require('fs')
const gm = require('gm')
Note.test = (userId, masterKey, printResults) => {
return false;
return new Promise((resolve, reject) => {
@@ -162,6 +163,10 @@ Note.test = (userId, masterKey, printResults) => {
return resolve('Test: Complete ---')
})
.catch(error => {
console.log(error)
return reject(error)
})
})
}
@@ -193,7 +198,7 @@ Note.create = (userId, noteTitle = '', noteText = '', masterKey) => {
})
.then((rows, fields) => {
if(SocketIo){
if(typeof SocketIo != 'undefined'){
SocketIo.to(userId).emit('new_note_created', rows[0].insertId)
}
@@ -341,7 +346,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
setTimeout(() => {
if(masterKey == null || note.salt == null){
console.log('Error indexing note', note.id)
console.log('Error indexing note - master key or salt missing', note.id)
return resolve(true)
}
@@ -390,13 +395,13 @@ Note.reindex = (userId, masterKey, removeId = null) => {
return Promise.all(reindexQueue)
})
.then(rawSearchIndex => {
.then(updatePromiseResults => {
const created = Math.round((+new Date)/1000)
const jsonSearchIndex = JSON.stringify(searchIndex)
const encryptedJsonIndex = cs.encrypt(masterKey, searchIndexSalt, jsonSearchIndex)
return db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
db.promise().query("UPDATE user_encrypted_search_index SET `index` = ?, `last_update` = ? WHERE (`user_id` = ?) LIMIT 1",
[encryptedJsonIndex, created, userId])
.then((rows, fields) => {
@@ -406,6 +411,7 @@ Note.reindex = (userId, masterKey, removeId = null) => {
.then((rows, fields) => {
// console.log('Indexd Note Count: ' + rows[0]['affectedRows'])
// @TODO - Return number of reindexed notes
resolve(true)
})
@@ -507,13 +513,13 @@ Note.update = (userId, noteId, noteText, noteTitle, color, pinned, archived, has
})
.then((rows, fields) => {
if(SocketIo){
if(typeof SocketIo != 'undefined'){
SocketIo.to(userId).emit('new_note_text_saved', {noteId, hash})
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
}
//Async attachment reindex
Attachment.scanTextForWebsites(SocketIo, userId, noteId, noteText)
//Send back updated response
resolve(rows[0])
})
@@ -739,12 +745,13 @@ Note.get = (userId, noteId, masterKey) => {
const nowTime = Math.round((+new Date)/1000)
db.promise().query(`UPDATE note SET opened = ? WHERE (id = ?)`, [nowTime, noteId])
//Return note data
// delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
.then(results => {
//Return note data
// delete noteData.salt //remove salt from return data
// delete noteData.encrypted_share_password_key
noteData.lockedOut = noteLockedOut
resolve(noteData)
})
})
.catch(error => {

View File

@@ -9,7 +9,8 @@ const speakeasy = require('speakeasy')
let User = module.exports = {}
const version = '3.6.3'
const version = '3.8.0'
// 3.7.3 - diff/patch update
//Login a user, if that user does not exist create them
//Issues login token
@@ -552,6 +553,12 @@ User.revokeActiveSessions = (userId, sessionId) => {
User.deleteUser = (userId, password) => {
if(!userId || !password){
return new Promise((resolve, reject) => {
return resolve('Missing User ID or Password. No Action Taken.')
})
}
//Verify user is correct by decryptig master key with password
let deletePromises = []
@@ -583,78 +590,4 @@ User.deleteUser = (userId, password) => {
//Remove all note attachments and files
return Promise.all(deletePromises)
}
User.keyPairTest = (testUserName = 'genMan', password = '1', printResults) => {
return new Promise((resolve, reject) => {
let masterKey = null
let testUserId = null
const randomUsername = Math.random().toString(36).substring(2, 15);
const randomPassword = '1'
const secondPassword = '2'
User.register(testUserName, password)
.then( ({ token, userId }) => {
testUserId = userId
if(printResults) console.log('Test: Register User '+testUserName+' - Pass')
return User.getMasterKey(testUserId, password)
})
.then(newMasterKey => {
masterKey = newMasterKey
if(printResults) console.log('Test: Generate/Decrypt Master Key - Pass')
return User.generateKeypair(testUserId, masterKey)
})
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
if(printResults) console.log(decryptedPrivate.toString('utf8'))
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
if(printResults) console.log(publicDeccryptMessage.toString('utf8'))
return User.login(testUserName, password)
})
.then( ({token, userId}) => {
if(printResults) console.log('Test: Login New User - Pass')
return User.changePassword(testUserId, randomPassword, secondPassword)
})
.then(passwordChangeResults => {
if(printResults) console.log('Test: Password Change - ', passwordChangeResults?'Pass':'Fail')
return User.login(testUserName, secondPassword)
})
.then(reLogin => {
if(printResults) console.log('Test: Login With new Password - Pass')
return User.getMasterKey(testUserId, secondPassword)
})
.then(newMasterKey => {
masterKey = newMasterKey
resolve({testUserId, masterKey})
})
})
}

View File

@@ -35,11 +35,6 @@ router.post('/textsearch', function (req, res) {
.then( data => res.send(data) )
})
router.post('/get', function (req, res) {
Attachment.forNote(userId, req.body.noteId)
.then( data => res.send(data) )
})
router.post('/update', function (req, res) {
Attachment.update(userId, req.body.attachmentId, req.body.updatedText, req.body.noteId)
.then( result => {
@@ -65,5 +60,26 @@ router.post('/upload', upload.single('file'), function (req, res, next) {
})
//
// Push URL to attachments
// push action on - public controller
//
// get push key
router.post('/getbookmarklet', function (req, res) {
Attachment.getPushkeyBookmarklet(userId)
.then( data => res.send(data) )
})
// generate new push key
router.post('/generatepushkey', function (req, res) {
})
// delete push key
router.post('/deletepushkey', function (req, res) {
})
module.exports = router

View File

@@ -4,6 +4,7 @@ const rateLimit = require('express-rate-limit')
const Note = require('@models/Note')
const User = require('@models/User')
const Attachment = require('@models/Attachment')
@@ -56,6 +57,29 @@ router.post('/register', registerLimiter, function (req, res) {
})
})
//
// Public Pushme Action
//
const pushMeLimiter = rateLimit({
windowMs: 30 * 60 * 1000, //30 min window
max: 50, // start blocking after x requests
message:'Error'
})
router.get('/pushmebaby', pushMeLimiter, function (req, res) {
Attachment.pushUrl(req.query.pushkey, req.query.url)
.then((() => {
const jsCode = `
<script>
window.close();
</script>
<h1>Posting URL</h1>
`;
res.header('Content-Security-Policy', "script-src 'unsafe-inline'");
res.set('Content-Type', 'text/html');
res.send(Buffer.from(jsCode));
}))
})
module.exports = router

View File

@@ -0,0 +1,100 @@
const Attachment = require('../../models/Attachment')
const User = require('../../models/User')
const testUserName = 'jestTestUserAttachment'
const password = 'Beans19934!!!'
let newUserId = null
let masterKey = null
let newPushKey = null
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
test('Test Generate Push Key', () => {
return Attachment.generatePushKey(newUserId)
.then( (pushKey) => {
newPushKey = pushKey
return Attachment.generatePushKey(newUserId)
})
.then( (pushKey) => {
// expect a long, defined pushkey
expect(pushKey).toBeDefined()
expect(pushKey?.length).toBeGreaterThan(20)
expect(pushKey).toMatch(newPushKey)
})
})
test('Test get Push Key Bookmarklet', () => {
return Attachment.getPushkeyBookmarklet(newUserId)
.then(( bookmarklet => {
// Expect a bookmarklet containting URL encoded pushkey from above
const keyCheck = bookmarklet.includes(encodeURIComponent(newPushKey))
expect(bookmarklet).toBeDefined()
expect(keyCheck).toBe(true)
}))
})
test('Test Push URL', () => {
let url = 'https://www.solidscribe.com'
return Attachment.pushUrl(newPushKey, url)
.then(( results => {
return Attachment.textSearch(newUserId, 'scribe')
}))
.then((results) => {
expect(results.length == 1).toBe(true)
})
})
test('Test Delete Push Key', () => {
return Attachment.deletePushKey(newUserId)
.then(( results => {
// Expect a true bool
expect(results).toBe(true)
}))
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -0,0 +1,117 @@
const Note = require('../../models/Note')
const User = require('../../models/User')
const testUserName = 'jestTestUserNote'
const password = 'Beans1234!!!'
const secondPassword = 'Rice1234!!!'
let newUserId = null
let masterKey = null
let testNoteId = 0
let testNoteId2 = 0
const searchWord1 = 'beans'
const searchWord2 = 'RICE'
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
test('Create Note', () => {
const noteTitle = 'Test Note'
const noteText = 'Some Note Text for Testing'
return Note.create(newUserId, noteTitle, noteText, masterKey)
.then((noteId) => {
testNoteId = noteId
expect(noteId).toBeGreaterThan(0)
})
})
test('Create Another Note', () => {
const noteTitle = 'Test Note2'
const noteText = 'Some Note Text for Testing more '+searchWord1
return Note.create(newUserId, noteTitle, noteText, masterKey)
.then((noteId) => {
testNoteId2 = noteId
expect(noteId).toBeGreaterThan(0)
})
})
test('Update a note', () => {
return Note.update(newUserId, testNoteId, updatedNoteText, 'title', 0, 0, 0, 'hash', masterKey)
.then((results) => {
expect(results.changedRows).toEqual(1)
})
})
test('Decrypt a note', () => {
return Note.get(newUserId, testNoteId, masterKey)
.then((noteData) => {
expect(noteData.text).toMatch(updatedNoteText)
})
})
test('Update note search index', () => {
return Note.reindex(newUserId, masterKey)
.then((results) => {
expect(results).toBe(true)
})
})
test('Search Encrypted Index', () => {
const searchString = `${searchWord1} ${searchWord2}`
return Note.encryptedIndexSearch(newUserId, searchString, null, masterKey)
.then(({ids}) => {
// Make sure beans is in one note and rice is in updated text
expect(ids.length).toEqual(2)
})
})
test('Search Encrypted Index no results', () => {
return Note.encryptedIndexSearch(newUserId, 'zzz', null, masterKey)
.then(({ids}) => {
// Make sure beans is in one note and rice is in updated text
expect(ids.length).toEqual(0)
})
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -0,0 +1,67 @@
const Note = require('../../models/Note')
const User = require('../../models/User')
const ShareNote = require('../../models/ShareNote')
const testUserName = 'jestTestUserNote'
const password = 'Beans1234!!!'
let newUserId = null
let masterKey = null
const testUserName2 = 'jestTestUserDude'
const password2 = 'Rice1234!!!'
let newUserId2 = null
let masterKey2 = null
let testNoteId = 0
let testNoteId2 = 0
// let sharedNoteId = 0 //ID of note shared with user
const shareUserId = 61
const searchWord1 = 'beans'
const searchWord2 = 'RICE'
const updatedNoteText = 'Some Note Text for Testing more '+searchWord2+' is nice'
beforeAll(() => {
// Find and Delete Previous Test user, log in, get key
return
User.getByUserName(testUserName)
.then(user => {
User.deleteUser(user?.id, password)
})
.then(user => {
User.getByUserName(testUserName2)
})
.then(user => {
User.deleteUser(user?.id, password)
})
.then((results) => {
return User.register(testUserName, password)
})
.then(({ token, userId }) => {
newUserId = userId
return User.getMasterKey(userId, password)
})
.then((newMasterKey) => {
masterKey = newMasterKey
return true
})
.catch(((error) => {
console.log(error)
}))
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -0,0 +1,112 @@
const User = require('../../models/User')
const crypto = require('crypto')
const testUserName = 'jestTestUser'
const password = 'Beans1234!!!'
const secondPassword = 'Rice1234!!!'
let testUserId = null
let masterKey = null
beforeAll(() => {
// Find and Delete Previous Test user
return User.getByUserName(testUserName)
.then((user) => {
return User.deleteUser(user?.id, password)
})
.then((results) => {
return results
})
})
test('Test User Registration', () => {
return User.register(testUserName, password)
.then((({ token, userId }) => {
testUserId = userId
expect(token).toBeDefined()
expect(userId).toBeGreaterThan(0)
}))
})
test('Test decrypting user masterKey', () => {
return User.getMasterKey(testUserId, password)
.then((newMasterKey) => {
masterKey = newMasterKey
expect(masterKey).toBeDefined()
})
})
test('Test generating public and private key pair', () => {
return User.generateKeypair(testUserId, masterKey)
.then(({publicKey, privateKey}) => {
const publicKeyMessage = 'Test: Public key decrypt - Pass'
const privateKeyMessage = 'Test: Private key decrypt - Pass'
//Encrypt Message with private Key
const privateKeyEncrypted = crypto.privateEncrypt(privateKey, Buffer.from(privateKeyMessage, 'utf8')).toString('base64')
const decryptedPrivate = crypto.publicDecrypt(publicKey, Buffer.from(privateKeyEncrypted, 'base64'))
//Conver back to a string
expect(decryptedPrivate.toString('utf8')).toMatch(privateKeyMessage)
//Encrypt with public key
const pubEncrMsc = crypto.publicEncrypt(publicKey, Buffer.from(publicKeyMessage, 'utf8')).toString('base64')
const publicDeccryptMessage = crypto.privateDecrypt(privateKey, Buffer.from(pubEncrMsc, 'base64') )
//Convert it back to string
expect(publicDeccryptMessage.toString('utf8')).toMatch(publicKeyMessage)
})
})
test('Test Logging in User', () => {
return User.login(testUserName, password)
.then(({token, userId}) => {
expect(token).toBeDefined()
expect(userId).toBeGreaterThan(0)
})
})
test('Test Changing Password', () => {
return User.changePassword(testUserId, password, secondPassword)
.then((passwordChangeResults) => {
expect(passwordChangeResults).toBe(true)
})
})
test('Test Login with wrong password', () => {
return User.login(testUserName, password)
.then(({token, userId}) => {
expect(token).toBeNull()
expect(userId).toBeNull()
})
})
test('Test decrypting masterKey with new Password', () => {
return User.getMasterKey(testUserId, secondPassword)
.then((newMasterKey) => {
expect(newMasterKey).toBeDefined()
expect(newMasterKey.length).toBe(28)
})
})
afterAll(done => {
// Close Database
const db = require('../../config/database')
db.end()
done()
})

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# Setup env variables
source ~/.env
# Send updated dynamic IP address to Namecheap, in order to update subdomains.
# This uses curl (separate pkg) to send the change; Namecheap automatically detects source IP if the ip field (like domain, password) ..
# is not specified.
# info helper
info() { printf "\n%s %s\n\n" "$( date )" "$*" >&2; }
info "Starting IP update for subdomains"
echo "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
# first subdomain
curl "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
# second subdomain
curl "https://dynamicdns.park-your-domain.com/update?host=$DYDNS_HOST2&domain=$DYDNS_DOMAIN&password=$DYDNS_PASS"
info "IP update done"