├── LICENSE ├── README.md ├── TimelineCalendar.js └── assets ├── oneside.jpeg ├── oneside_withallday.jpeg ├── twosides.jpeg └── twosides_withallday.jpeg /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 scriptable-js 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TimelineCalendar 2 | 3 | This is a javascript file which works with scriptable app (for iOS/Mac). 4 | 5 | # Usage 6 | 7 | Download the Javascript file to your scriptable directory and execute it. 8 | 9 | # Configuration 10 | 11 | This widget can be configured in multiple ways, 12 | 13 | ## Widget with text on on side (saves space) 14 | 15 | 16 | ![Oneside](https://github.com/scriptable-js/TimelineCalendar/blob/main/assets/oneside.jpeg?raw=true) 17 | 18 | You can either update the `DEFAULT_PARAMS` or provide the parameters as a json object from the widget. 19 | 20 | `textToRight: true, // Saves space by adding all texts to once side -->` 21 | 22 | or 23 | 24 | `{ "textToRight" : true}` // as a parameter into the widget 25 | 26 | ## Other configurations are listed below, feel free to update these as you wish 27 | 28 | ``` 29 | bg: "medium-top", // background image to use 30 | width: 450, 31 | height: 250, 32 | hoursToShow: 3, 33 | calendars: [], // All calendars by default 34 | excludeCalendars: [], // Exclude superceds selected calendar 35 | lineWidth: 10, 36 | ellipseWidth: 20, 37 | allDayEvents: true, // Uncoment this or add to widget parameter to show two column events 38 | textToRight: false, // Saves space by adding all texts to once side 39 | ``` 40 | 41 | ## 42 | 43 | ![Oneside with all day events](https://github.com/scriptable-js/TimelineCalendar/blob/main/assets/oneside_withallday.jpeg?raw=true) 44 | 45 | ![Twosides with all day events](https://github.com/scriptable-js/TimelineCalendar/blob/main/assets/twosides_withallday.jpeg?raw=true) 46 | 47 | ![Twosides](https://github.com/scriptable-js/TimelineCalendar/blob/main/assets/twosides.jpeg?raw=true) -------------------------------------------------------------------------------- /TimelineCalendar.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: deep-blue; icon-glyph: magic; 4 | // Variables used by Scriptable. 5 | // These must be at the very top of the file. Do not edit. 6 | // icon-color: teal; icon-glyph: magic; 7 | 8 | // ------------Parameters------------// 9 | const DEFAULT_COLOR_HEX = '#ffffff'; 10 | const DEFAULT_COLOR = new Color(DEFAULT_COLOR_HEX, 0.3); 11 | const HALF_HOUR_COLOR = new Color(DEFAULT_COLOR_HEX, 0.6); 12 | 13 | DEFAULT_PARAMS = { 14 | bg: "medium-top", // background image to use 15 | width: 450, 16 | height: 250, 17 | hoursToShow: 3, 18 | calendars: [], // All calendars by default 19 | excludeCalendars: [], // Exclude superceds selected calendar 20 | lineWidth: 10, 21 | ellipseWidth: 20, 22 | allDayEvents: true, // Uncoment this or add to widget parameter to show two column events 23 | textToRight: true, // Saves space by adding all texts to once side 24 | }; 25 | 26 | Date.prototype.addHours = function (numHours) { 27 | const date = new Date(this.valueOf()); 28 | date.setHours(date.getHours() + numHours); 29 | return date; 30 | }; 31 | 32 | Date.prototype.addMinutes = function (numMinutes) { 33 | const date = new Date(this.valueOf()); 34 | date.setMinutes(date.getMinutes() + numMinutes); 35 | return date; 36 | }; 37 | 38 | 39 | // ------------Utility functions ------------// 40 | addStack = (el, type = 'horizontal', centered = false, size) => { 41 | const stack = el.addStack() 42 | if (type === 'vertical') stack.layoutVertically() 43 | else stack.layoutHorizontally() 44 | 45 | if (centered) 46 | stack.centerAlignContent() 47 | 48 | if (size) stack.size = size 49 | return stack 50 | } 51 | 52 | addText = (el, string, type, size = 9.5) => { 53 | const text = el.addText(string) 54 | text.font = type === 'bold' ? 55 | Font.boldSystemFont(size * 1.2) : 56 | Font.regularSystemFont(size) 57 | text.textOpacity = type === 'small' ? 0.5 : 1 58 | text.lineLimit = 2 59 | text.centerAlignText() 60 | return text; 61 | } 62 | 63 | addCenteredText = (el, text, type, size = 9.5) => { 64 | const stack = addStack(el, 'horizontal', true) 65 | stack.addSpacer() 66 | const textObj = addText(stack, text, type, size) 67 | stack.addSpacer() 68 | return textObj; 69 | } 70 | 71 | // ------------Class------------// 72 | class TimelineCalendar { 73 | constructor(dParams, widget) { 74 | // parameters provided to the class overrides default parameters. 75 | this.params = { ...DEFAULT_PARAMS, ...dParams }; 76 | this.widget = widget ? widget : this.initWidget(); 77 | this.now = new Date(); 78 | 79 | var context = new DrawContext(); 80 | context.size = new Size(this.params.width, this.params.height); 81 | context.opaque = false; 82 | context.respectScreenScale = true; 83 | this.drawContext = context; 84 | 85 | this.xMiddlePosition = this.params.textToRight ? 10 : this.params.width / 2; 86 | 87 | this.colors = [ 88 | Color.orange(), 89 | Color.cyan(), 90 | Color.blue(), 91 | Color.yellow(), 92 | Color.green(), 93 | Color.magenta(), 94 | Color.brown(), 95 | ]; 96 | } 97 | 98 | drawLine = async (x, y, width, height, color) => { 99 | const path = new Path(); 100 | path.addRect(new Rect(x, y, width, height)); 101 | this.drawContext.addPath(path); 102 | this.drawContext.setFillColor(color); 103 | this.drawContext.fillPath(); 104 | }; 105 | 106 | drawHalfHourLines = async () => { 107 | const halfHours = this.params.hoursToShow * 2 + 1; 108 | const halfHourEllipseWidth = this.params.ellipseWidth * 1.2; 109 | var timeNow = new Date(); 110 | timeNow.setMinutes(0); 111 | timeNow.setSeconds(0); 112 | for (var i = 0; i < halfHours; i++) { 113 | timeNow = timeNow.addMinutes(30); 114 | const xValue = 115 | this.xMiddlePosition - 116 | (halfHourEllipseWidth - this.params.lineWidth) / 2; 117 | const scaleYValue = this.scaleTimeToPixles(timeNow); 118 | this.drawEllipse( 119 | xValue, 120 | scaleYValue, 121 | halfHourEllipseWidth, 122 | 2, 123 | HALF_HOUR_COLOR 124 | ); 125 | } 126 | }; 127 | 128 | drawEllipse = async (x, y, width, height, color = DEFAULT_COLOR) => { 129 | const path = new Path(); 130 | path.addEllipse(new Rect(x, y, width, height)); 131 | 132 | this.drawContext.setFillColor(color); 133 | this.drawContext.setTextColor(color); 134 | this.drawContext.addPath(path); 135 | this.drawContext.fillPath(); 136 | }; 137 | 138 | scaleTimeToPixles = (timeToScale) => { 139 | // Convert time to pixles on the yaxis 140 | const height = this.params.height; 141 | const totalDurationInMinutes = this.params.hoursToShow * 60; 142 | const pixlesPerMinute = Math.round(height / totalDurationInMinutes); 143 | 144 | var timeAtHour = new Date(); 145 | timeAtHour.setSeconds(0); 146 | timeAtHour = timeAtHour.setMinutes(0); 147 | 148 | const timeInMinutes = Math.round( 149 | Math.abs(timeAtHour - timeToScale) / (1000 * 60) 150 | ); // Milliseconds to minutes 151 | 152 | return pixlesPerMinute * timeInMinutes; // pixles/minute * minute = pixles 153 | }; 154 | 155 | drawCurrentTime = async () => { 156 | var scaledVal = this.scaleTimeToPixles(this.now); 157 | const xValue = this.xMiddlePosition - (30 - 10) / 2; 158 | this.drawEllipse(xValue, scaledVal, 30, 10, Color.white()); 159 | }; 160 | 161 | isInRange = (value, begin, end) => { 162 | return value >= begin && value <= end; 163 | }; 164 | 165 | getEvents = async () => { 166 | const today = await CalendarEvent.today([]); 167 | const tomorrow = await CalendarEvent.tomorrow([]); 168 | const events = today.concat(tomorrow); 169 | 170 | var eventsToBeDisplayed = []; 171 | var alldayEvents = []; 172 | 173 | var currentHour = new Date(); 174 | currentHour.setMinutes(0); 175 | currentHour.setSeconds(0); 176 | 177 | var endHour = new Date(); 178 | endHour = endHour.addHours(this.params.hoursToShow); 179 | 180 | events.forEach((event) => { 181 | const start = new Date(event.startDate); 182 | const end = new Date(event.endDate); 183 | 184 | if (!event.title) return; // No title = no calendar event 185 | //if (event.isAllDay) return; // Don't worry about all day events 186 | 187 | if (event.isAllDay) { 188 | if (start.getDate() == this.now.getDate()) alldayEvents.push(event); 189 | return; 190 | } 191 | 192 | var dupe = false; 193 | eventsToBeDisplayed.forEach((e) => { 194 | if (e.event.title == event.title) { 195 | dupe = true; 196 | return; // cannot break a forEach loop, need to return anoymouse function. 197 | } 198 | }); 199 | 200 | if (dupe) return; 201 | 202 | const isStartInRange = this.isInRange(start, currentHour, endHour); 203 | const isEndInRange = this.isInRange(end, currentHour, endHour); 204 | const eventSubsumesRange = start <= currentHour && end >= endHour; 205 | const eventInSelectedCalendar = 206 | this.params.calendars.length === 0 || 207 | this.params.calendars.includes(event.calendar.title); 208 | 209 | const eventCalendarExcluded = this.params.excludeCalendars.includes( 210 | event.calendar.title 211 | ); 212 | 213 | if (eventCalendarExcluded) return; 214 | 215 | if ( 216 | (isStartInRange || isEndInRange || eventSubsumesRange) && 217 | eventInSelectedCalendar 218 | ) { 219 | var eventObj = { 220 | start: start, 221 | end: end, 222 | event: event, 223 | isStartInRange: isStartInRange, 224 | isEndInRange: isEndInRange, 225 | }; 226 | eventsToBeDisplayed.push(eventObj); 227 | } 228 | }); 229 | 230 | const allEvents = { 231 | timelineEvents: eventsToBeDisplayed, 232 | alldayEvents: alldayEvents, 233 | }; 234 | return allEvents; 235 | }; 236 | 237 | formatDateValue = (data) => { 238 | return ("0" + data).slice(-2); 239 | }; 240 | 241 | drawEventsOnTheHour = async () => { 242 | const allEvents = await this.getEvents(); 243 | const events = allEvents.timelineEvents; 244 | for (let [index, event] of Object.entries(events)) { 245 | const isEven = index % 2 === 0 && !this.params.textToRight; 246 | const color = this.colors[index % this.colors.length]; 247 | 248 | var startY = 0; 249 | var endY = this.params.height; 250 | if (event.isStartInRange) startY = this.scaleTimeToPixles(event.start); 251 | if (event.isEndInRange) endY = this.scaleTimeToPixles(event.end); 252 | 253 | // Change this line if you want the text to be shown to one side. 254 | if (isEven) this.drawContext.setTextAlignedRight(); 255 | else this.drawContext.setTextAlignedLeft(); 256 | 257 | this.drawContext.setFillColor(color); 258 | this.drawContext.setTextColor(color); 259 | 260 | const textRect = new Rect( 261 | isEven ? 0 : this.xMiddlePosition + 20, 262 | startY, 263 | this.params.width / 2 - 20, 264 | 20 265 | ); 266 | const timeText = ` (${event.start.toLocaleString("en-US", { 267 | hour: "numeric", 268 | minute: "numeric", 269 | hour12: true, 270 | })}) `; 271 | 272 | // Move the time near the line 273 | const text = isEven 274 | ? event.event.title + timeText 275 | : timeText + event.event.title; 276 | this.drawContext.drawTextInRect(text, textRect); 277 | 278 | // Position the ellipse a the center of the line. Since they both start 279 | // at the same 'x' we need to subtract some to get to the center. 280 | const xValue = 281 | this.xMiddlePosition - 282 | (this.params.ellipseWidth - this.params.lineWidth) / 2; 283 | this.drawEllipse(xValue, startY, this.params.ellipseWidth, 2, color); 284 | this.drawEllipse(xValue, endY, this.params.ellipseWidth, 2, color); 285 | this.drawLine( 286 | this.xMiddlePosition, 287 | startY, 288 | this.params.lineWidth, 289 | endY - startY, 290 | color 291 | ); 292 | } 293 | return allEvents; 294 | }; 295 | 296 | handleAlldayEvents = async (stack, events) => { 297 | const text = 298 | events.length === 0 ? "All Day Events? - Nope" : "All Day Events"; 299 | console.log("da faq"); 300 | addCenteredText(stack, text, "Bold", 8).textColor = new Color(DEFAULT_COLOR_HEX); 301 | console.log("da faq"); 302 | stack.addSpacer(10); 303 | console.log("da faq"); 304 | 305 | console.log(events); 306 | 307 | events.forEach((e) => { 308 | addCenteredText( 309 | stack, 310 | `-> ${e.title}`, 311 | "normal", 312 | 8 313 | ).color = new Color(`#${e.calendar.color.hex}`); 314 | }); 315 | stack.addSpacer(); 316 | }; 317 | 318 | drawCalendarWidget = async (stack) => { 319 | const row1 = addStack(stack); 320 | const vStack1 = addStack(row1, "vertical", false); 321 | 322 | await this.drawLine( 323 | this.xMiddlePosition, 324 | 0, // y 325 | 10, // width 326 | this.params.height, 327 | DEFAULT_COLOR 328 | ); 329 | await this.drawHalfHourLines(); 330 | await this.drawCurrentTime(); 331 | const allEvents = await this.drawEventsOnTheHour(); 332 | 333 | vStack1.addImage(this.drawContext.getImage()); 334 | 335 | if (this.params.allDayEvents) { 336 | const vStack2 = addStack(row1, "vertical", false, new Size(80,0)); 337 | this.handleAlldayEvents(vStack2, allEvents.alldayEvents); 338 | vStack2.addSpacer(); 339 | } 340 | }; 341 | 342 | // Use this method to create a widet it this is a standalone widget. 343 | initWidget() { 344 | return new ListWidget(); 345 | } 346 | } 347 | 348 | // Note you can use this class as a module and import it other widgets, 349 | // Just call 'drawCalendarWidget' with a new stack. 350 | // module.exports = LineCalendar; 351 | 352 | // ------------Widget Code------------// 353 | // parameters provided to the class overrides default parameters. 354 | // Check the default parameters at the top of the file for more info. 355 | var params = {} 356 | if (args.widgetParameter != null) params = JSON.parse(args.widgetParameter); 357 | 358 | const lineCalendar = new TimelineCalendar(params); 359 | 360 | const w = lineCalendar.initWidget(); 361 | // var stack = addStack(w, "vertical", true); 362 | await lineCalendar.drawCalendarWidget(w); 363 | // w.backgroundImage = files.readImage(lineCalendar.params.bg); // use your own background here 364 | w.backgroundColor = Color.black(); 365 | 366 | w.presentMedium(); 367 | Script.setWidget(w); 368 | Script.complete(); 369 | -------------------------------------------------------------------------------- /assets/oneside.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptable-js/TimelineCalendar/7bfa1ab2653ad857f086594e4ab6fba7bb323f84/assets/oneside.jpeg -------------------------------------------------------------------------------- /assets/oneside_withallday.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptable-js/TimelineCalendar/7bfa1ab2653ad857f086594e4ab6fba7bb323f84/assets/oneside_withallday.jpeg -------------------------------------------------------------------------------- /assets/twosides.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptable-js/TimelineCalendar/7bfa1ab2653ad857f086594e4ab6fba7bb323f84/assets/twosides.jpeg -------------------------------------------------------------------------------- /assets/twosides_withallday.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scriptable-js/TimelineCalendar/7bfa1ab2653ad857f086594e4ab6fba7bb323f84/assets/twosides_withallday.jpeg --------------------------------------------------------------------------------