-
Notifications
You must be signed in to change notification settings - Fork 14
/
daysUntilBirthdayWidget.js
269 lines (234 loc) · 9.77 KB
/
daysUntilBirthdayWidget.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
//////////////////////////////////
// how to use:
// Mode 1 (default): Only chosen contacts:
// in your contacts app, edit the contacts you want to be visible in this widget
// -> you need to set up an additional 'date' field in your contact and give the date the label 'daysUntilBirthday'
// run the script initially in the Scriptable app to create a json file in iCloud containing contact information for faster access
// when you add new contacts via the label, run the script again in the app to update the json and make the changes visible in iCloud-mode
// when setting the script up as Widget, use the largest presentation mode and provide the parameter 'iCloud' (without the '')
//
// Mode 2: Show all contacts with a Birthday configured
// set the next variable to true or provide the parameter 'showAll' in widget mode to show all contacts that have a birthday in the regular birthday field configured
//
let showAllContacts = false;
//
// iCloud-Mode:
// set the next variable to true or provide the parameter 'iCloud' in widget mode to never recalculate which contacts are shown again
// if false -> everytime the contacts are scanned
// if true -> contacts are not scanned and last used contacts are used again
let useIcloud = false;
//////////////////////////////////
// the label name you need to set in contacts on the first date field
const contactNotesKeyWord = 'daysUntilBirthday';
//////////////////////////////////
// edit these according to your language
const daysUntilBirthdayText = 'Tage bis zum Geburtstag von';
const daysText = 'Tage';
const todayText = 'Heute!';
//////////////////////////////////
const dateFormatter = new DateFormatter();
dateFormatter.dateFormat = 'dd.MM.yyyy';
const timeFormatter = new DateFormatter();
timeFormatter.dateFormat = 'dd.MM.yyyy HH:mm:ss';
const headerFont = new Font('Menlo-regular', 14);
const contactNameFont = new Font('Menlo-regular', 17);
const smallInfoFont = new Font('Menlo-regular', 10);
const updatedAtFont = new Font('Menlo-regular', 7);
const fontColorGrey = new Color("#918A8A");
const fontColorWhite = new Color("#FFFFFF");
// used for inserting space characters
const lineLength = 7;
// class that is also serialized to a json file in iCloud
class CustomContact {
constructor(name, daysUntil, date) {
this.name = name;
this.daysUntil = daysUntil;
this.date = date;
}
getAsKey() {
// name and daysUntil together make the contact unique
return this.name + '-' + this.daysUntil;
}
}
const widget = await createWidget();
widget.backgroundColor = new Color("#000000");
if (!config.runsInWidget) {
await widget.presentLarge();
}
Script.setWidget(widget);
Script.complete();
async function createWidget() {
// Overwrite the default values on top when running as widget
// working parameter examples: 'iCloud,showAll', 'showAll,iCloud', 'iCloud', 'showAll'
if (args.widgetParameter) {
if (args.widgetParameter.includes('iCloud')) {
useIcloud = true;
}
if (args.widgetParameter.includes('showAll')) {
showAllContacts = true;
}
}
const widget = new ListWidget();
let headerRow = widget.addStack();
let headerText = headerRow.addText(daysUntilBirthdayText);
headerText.textColor = fontColorGrey;
headerText.font = headerFont;
let shownCustomContacts = [];
let dataSource = '';
// enter 'iCloud' without the '' as parameter when setting up the script as a widget
if (useIcloud) {
dataSource = 'iCloud';
shownCustomContacts = loadCustomContacts();
updateCustomContacts(shownCustomContacts);
} else {
dataSource = 'iPhone';
let containers = await ContactsContainer.all();
let contactsInIos = await Contact.all(containers);
let keysForDuplicatePrevention = [];
for (let contact of contactsInIos) {
let dateToUse = null; // if set, a contact is found
if (showAllContacts) {
// Mode 2: show all contacts with a regular birthday field set
if (contact.isBirthdayAvailable) {
dateToUse = contact.birthday;
dateToUse = getFixedDate(dateToUse);
}
} else {
// Mode 1: only show chosen contacts
// contacts need to have an additional date property named like the content of variable contactNotesKeyWord
if (contact.dates) {
for (let date of contact.dates) {
if (date.label.startsWith(contactNotesKeyWord)) {
dateToUse = date.value;
dateToUse = getFixedDate(dateToUse);
}
}
}
}
if (!dateToUse) {
// contact should not be shown -> continue to the next contact
continue;
}
// if here: contact will be shown
// the shorter nickname is preferred
let contactsName = contact.nickname ? contact.nickname : contact.givenName;
// next line removes emoji that come after a space character
contactsName = contactsName.split(' ')[0];
let foundContact = new CustomContact(contactsName, calculateDaysUntil(dateToUse), dateFormatter.string(new Date(dateToUse)));
// check if already found before (in case of multiple contact containers)
if (!keysForDuplicatePrevention.includes(foundContact.getAsKey())) {
keysForDuplicatePrevention.push(foundContact.getAsKey());
shownCustomContacts.push(foundContact);
}
}
}
// sorts contacts by how near their birthday is
shownCustomContacts.sort(function (a, b) {
return a.daysUntil > b.daysUntil;
});
// write back to json in iCloud
saveCustomContacts(shownCustomContacts);
// this row consists of two customContact infos
let currentRow;
// counter for creating two columns and a maximum of 20 visible contacts
let contactCounter = 0;
for (let customContact of shownCustomContacts) {
if (contactCounter === 20) {
// only the top 20 earliest birthdays are shown in the widget
break;
}
if (contactCounter % 2 === 0) {
// start a new row
currentRow = widget.addStack();
}
addContactInfoToRow(customContact, currentRow);
contactCounter++;
if (contactCounter < 20) {
widget.addSpacer(1);
}
}
let updatedAt = widget.addText('Source: ' + dataSource + ', Update: ' + timeFormatter.string(new Date()));
updatedAt.font = updatedAtFont;
updatedAt.textColor = fontColorWhite;
updatedAt.centerAlignText();
return widget;
}
// used to align the information
function addSpaces(amount, row) {
for (let i = 0; i < amount; i++) {
let text = row.addText(' ');
text.font = contactNameFont;
}
}
function addContactInfoToRow(customContact, row) {
addSpaces(lineLength - customContact.name.length, row);
let nameRow = row.addText(customContact.name);
nameRow.font = contactNameFont;
nameRow.textColor = fontColorWhite;
let actualText = customContact.daysUntil === 0 ? ' ' + todayText + '\n ' + customContact.date.replace('.2222', '.????') : ' ' + customContact.daysUntil + ' ' + daysText + '\n ' + customContact.date.replace('.2222', '.????');
let daysInfoText = row.addText(actualText);
daysInfoText.textColor = fontColorGrey;
daysInfoText.font = smallInfoFont;
}
function calculateDaysUntil(birthdayString) {
let startDate = new Date();
let targetDate = new Date(birthdayString);
targetDate.setFullYear(startDate.getFullYear());
let timeRemaining = parseInt((targetDate.getTime() - startDate.getTime()) / 1000);
if (timeRemaining < 0) {
// the date was in the past -> recalculate for next year
targetDate.setFullYear(targetDate.getFullYear() + 1);
timeRemaining = parseInt((targetDate.getTime() - startDate.getTime()) / 1000);
}
if (timeRemaining >= 0) {
let days = 1 + parseInt(timeRemaining / 86400);
return parseInt(days, 10) % 365;
} else {
return '???';
}
}
// recalculates the daysUntil value of the customContacts
function updateCustomContacts(customContacts) {
for (let contact of customContacts) {
let date = dateFormatter.date(contact.date);
date = getFixedDate(date);
contact.daysUntil = calculateDaysUntil(date.toString());
}
}
// loads contacts stored in the json
function loadCustomContacts() {
// this could be changed to FileManager.local() if you don't want to use iCloud
let fm = FileManager.iCloud();
let path = getFilePath();
if (fm.fileExists(path)) {
let raw = fm.readString(path);
return JSON.parse(raw);
} else {
return [];
}
}
// Saves the CustomContacts to a file in iCloud Drive.
function saveCustomContacts(customContacts) {
// this could be changed to FileManager.local() if you don't want to use iCloud
let fm = FileManager.iCloud();
let path = getFilePath();
let raw = JSON.stringify(customContacts);
fm.writeString(path, raw);
}
// Gets path of the file containing the stored CustomContact data. Creates the file if necessary.
function getFilePath() {
let fm = FileManager.iCloud();
let dirPath = fm.joinPath(fm.documentsDirectory(), "daysUntilBirthdayData");
if (!fm.fileExists(dirPath)) {
fm.createDirectory(dirPath);
}
return fm.joinPath(dirPath, "customContacts.json");
}
function getFixedDate(date) {
if (date?.getFullYear() === 1) {
// crazy bug in ios contacts if no year is set...
date = new Date(2222, date.getMonth(), date.getDate());
date.setDate(date.getDate() + 2);
}
return date;
}