+32
-2
api.js
+32
-2
api.js
···
290
return await this.getRequest('app.bsky.notification.listNotifications', params);
291
}
292
293
-
/** @param {string} [cursor], @returns {Promise<{ cursor: string | undefined, posts: json[] }>} */
294
295
async loadMentions(cursor) {
296
let response = await this.loadNotifications(cursor);
···
306
return { cursor: response.cursor, posts };
307
}
308
309
-
/** @param {number} days, @returns {Promise<json[]>} */
310
311
async loadTimeline(days, options = {}) {
312
let now = new Date();
313
let timeLimit = now.getTime() - days * 86400 * 1000;
314
315
return await this.fetchAll('app.bsky.feed.getTimeline', { limit: 100 }, {
316
field: 'feed',
317
breakWhen: (x) => {
318
let timestamp = x.reason ? x.reason.indexedAt : x.post.record.createdAt;
···
290
return await this.getRequest('app.bsky.notification.listNotifications', params);
291
}
292
293
+
/**
294
+
* @param {string} [cursor]
295
+
* @returns {Promise<{ cursor: string | undefined, posts: json[] }>}
296
+
*/
297
298
async loadMentions(cursor) {
299
let response = await this.loadNotifications(cursor);
···
309
return { cursor: response.cursor, posts };
310
}
311
312
+
/**
313
+
* @param {number} days
314
+
* @param {{ onPageLoad?: FetchAllOnPageLoad }} [options]
315
+
* @returns {Promise<json[]>}
316
+
*/
317
318
async loadTimeline(days, options = {}) {
319
let now = new Date();
320
let timeLimit = now.getTime() - days * 86400 * 1000;
321
322
return await this.fetchAll('app.bsky.feed.getTimeline', { limit: 100 }, {
323
+
field: 'feed',
324
+
breakWhen: (x) => {
325
+
let timestamp = x.reason ? x.reason.indexedAt : x.post.record.createdAt;
326
+
return Date.parse(timestamp) < timeLimit;
327
+
},
328
+
onPageLoad: options.onPageLoad
329
+
});
330
+
}
331
+
332
+
/**
333
+
* @param {string} did
334
+
* @param {number} days
335
+
* @param {{ onPageLoad?: FetchAllOnPageLoad }} [options]
336
+
* @returns {Promise<json[]>}
337
+
*/
338
+
339
+
async loadUserTimeline(did, days, options = {}) {
340
+
let now = new Date();
341
+
let timeLimit = now.getTime() - days * 86400 * 1000;
342
+
343
+
let params = { actor: did, filter: 'posts_no_replies', limit: 100 };
344
+
345
+
return await this.fetchAll('app.bsky.feed.getAuthorFeed', params, {
346
field: 'feed',
347
breakWhen: (x) => {
348
let timestamp = x.reason ? x.reason.indexedAt : x.post.record.createdAt;
+30
index.html
+30
index.html
···
127
</table>
128
</div>
129
130
<script src="lib/purify.min.js"></script>
131
<script src="minisky.js"></script>
132
<script src="api.js"></script>
133
<script src="utils.js"></script>
134
<script src="rich_text_lite.js"></script>
135
<script src="models.js"></script>
136
<script src="thread_page.js"></script>
137
<script src="posting_stats_page.js"></script>
138
<script src="embed_component.js"></script>
139
<script src="post_component.js"></script>
140
<script src="skythread.js"></script>
···
127
</table>
128
</div>
129
130
+
<div id="like_stats_page">
131
+
<h2>Like statistics</h2>
132
+
133
+
<form>
134
+
<p>
135
+
Time range: <input type="range" min="1" max="60" value="7"> <label>7 days</label>
136
+
</p>
137
+
138
+
<p>
139
+
<input type="submit" value="Start scan"> <progress></progress>
140
+
</p>
141
+
</form>
142
+
143
+
<table class="scan-result given-likes">
144
+
<thead>
145
+
<tr><th colspan="3">❤️ Likes from you:</th></tr>
146
+
</thead>
147
+
<tbody></tbody>
148
+
</table>
149
+
150
+
<table class="scan-result received-likes">
151
+
<thead>
152
+
<tr><th colspan="3">💛 Likes on your posts:</th></tr>
153
+
</thead>
154
+
<tbody></tbody>
155
+
</table>
156
+
</div>
157
+
158
<script src="lib/purify.min.js"></script>
159
<script src="minisky.js"></script>
160
<script src="api.js"></script>
161
<script src="utils.js"></script>
162
<script src="rich_text_lite.js"></script>
163
<script src="models.js"></script>
164
+
<script src="menu.js"></script>
165
<script src="thread_page.js"></script>
166
<script src="posting_stats_page.js"></script>
167
+
<script src="like_stats_page.js"></script>
168
<script src="embed_component.js"></script>
169
<script src="post_component.js"></script>
170
<script src="skythread.js"></script>
+282
like_stats_page.js
+282
like_stats_page.js
···
···
1
+
class LikeStatsPage {
2
+
3
+
/** @type {number | undefined} */
4
+
scanStartTime;
5
+
6
+
constructor() {
7
+
this.pageElement = $id('like_stats_page');
8
+
9
+
this.rangeInput = $(this.pageElement.querySelector('input[type="range"]'), HTMLInputElement);
10
+
this.submitButton = $(this.pageElement.querySelector('input[type="submit"]'), HTMLInputElement);
11
+
this.progressBar = $(this.pageElement.querySelector('input[type=submit] + progress'), HTMLProgressElement);
12
+
13
+
this.receivedTable = $(this.pageElement.querySelector('.received-likes'), HTMLTableElement);
14
+
this.givenTable = $(this.pageElement.querySelector('.given-likes'), HTMLTableElement);
15
+
16
+
this.appView = new BlueskyAPI('public.api.bsky.app', false);
17
+
18
+
this.setupEvents();
19
+
20
+
this.progressPosts = 0;
21
+
this.progressLikeRecords = 0;
22
+
this.progressPostLikes = 0;
23
+
}
24
+
25
+
setupEvents() {
26
+
$(this.pageElement.querySelector('form')).addEventListener('submit', (e) => {
27
+
e.preventDefault();
28
+
29
+
if (!this.scanStartTime) {
30
+
this.findLikes();
31
+
} else {
32
+
this.stopScan();
33
+
}
34
+
});
35
+
36
+
this.rangeInput.addEventListener('input', (e) => {
37
+
let days = parseInt(this.rangeInput.value, 10);
38
+
let label = $(this.pageElement.querySelector('input[type=range] + label'));
39
+
label.innerText = (days == 1) ? '1 day' : `${days} days`;
40
+
});
41
+
}
42
+
43
+
/** @returns {number} */
44
+
45
+
selectedDaysRange() {
46
+
return parseInt(this.rangeInput.value, 10);
47
+
}
48
+
49
+
show() {
50
+
this.pageElement.style.display = 'block';
51
+
}
52
+
53
+
/** @returns {Promise<void>} */
54
+
55
+
async findLikes() {
56
+
this.submitButton.value = 'Cancel';
57
+
58
+
let requestedDays = this.selectedDaysRange();
59
+
60
+
this.resetProgress();
61
+
this.progressBar.style.display = 'inline';
62
+
63
+
let startTime = new Date().getTime();
64
+
this.scanStartTime = startTime;
65
+
66
+
this.receivedTable.style.display = 'none';
67
+
this.givenTable.style.display = 'none';
68
+
69
+
let fetchGivenLikes = this.fetchGivenLikes(requestedDays);
70
+
71
+
let receivedLikes = await this.fetchReceivedLikes(requestedDays);
72
+
let receivedStats = this.sumUpReceivedLikes(receivedLikes);
73
+
let topReceived = this.getTopEntries(receivedStats);
74
+
75
+
await this.renderResults(topReceived, this.receivedTable);
76
+
77
+
let givenLikes = await fetchGivenLikes;
78
+
let givenStats = this.sumUpGivenLikes(givenLikes);
79
+
let topGiven = this.getTopEntries(givenStats);
80
+
81
+
let profileInfo = await appView.getRequest('app.bsky.actor.getProfiles', { actors: topGiven.map(x => x.did) });
82
+
83
+
for (let profile of profileInfo.profiles) {
84
+
let user = /** @type {LikeStat} */ (topGiven.find(x => x.did == profile.did));
85
+
user.handle = profile.handle;
86
+
user.avatar = profile.avatar;
87
+
}
88
+
89
+
await this.renderResults(topGiven, this.givenTable);
90
+
91
+
this.receivedTable.style.display = 'table';
92
+
this.givenTable.style.display = 'table';
93
+
94
+
this.submitButton.value = 'Start scan';
95
+
this.progressBar.style.display = 'none';
96
+
this.scanStartTime = undefined;
97
+
}
98
+
99
+
/** @param {number} requestedDays, @returns {Promise<json[]>} */
100
+
101
+
async fetchGivenLikes(requestedDays) {
102
+
let startTime = /** @type {number} */ (this.scanStartTime);
103
+
104
+
return await accountAPI.fetchAll('com.atproto.repo.listRecords', {
105
+
repo: accountAPI.user.did,
106
+
collection: 'app.bsky.feed.like',
107
+
limit: 100
108
+
}, {
109
+
field: 'records',
110
+
breakWhen: (x) => Date.parse(x['value']['createdAt']) < startTime - 86400 * requestedDays * 1000,
111
+
onPageLoad: (data) => {
112
+
if (data.length == 0) { return }
113
+
114
+
let last = data[data.length - 1];
115
+
let lastDate = Date.parse(last.value.createdAt);
116
+
117
+
let daysBack = (startTime - lastDate) / 86400 / 1000;
118
+
this.updateProgress({ likeRecords: Math.min(1.0, daysBack / requestedDays) });
119
+
}
120
+
});
121
+
}
122
+
123
+
/** @param {number} requestedDays, @returns {Promise<json[]>} */
124
+
125
+
async fetchReceivedLikes(requestedDays) {
126
+
let startTime = /** @type {number} */ (this.scanStartTime);
127
+
128
+
let myPosts = await this.appView.loadUserTimeline(accountAPI.user.did, requestedDays, {
129
+
onPageLoad: (data) => {
130
+
if (data.length == 0) { return }
131
+
132
+
let last = data[data.length - 1];
133
+
let lastTimestamp = last.reason ? last.reason.indexedAt : last.post.record.createdAt;
134
+
let lastDate = Date.parse(lastTimestamp);
135
+
136
+
let daysBack = (startTime - lastDate) / 86400 / 1000;
137
+
this.updateProgress({ posts: Math.min(1.0, daysBack / requestedDays) });
138
+
}
139
+
});
140
+
141
+
let likedPosts = myPosts.filter(x => !x['reason'] && x['post']['likeCount'] > 0);
142
+
143
+
let results = [];
144
+
145
+
for (let i = 0; i < likedPosts.length; i += 10) {
146
+
let batch = likedPosts.slice(i, i + 10);
147
+
this.updateProgress({ postLikes: i / likedPosts.length });
148
+
149
+
let fetchBatch = batch.map(x => {
150
+
return this.appView.fetchAll('app.bsky.feed.getLikes', { uri: x['post']['uri'], limit: 100 }, {
151
+
field: 'likes'
152
+
});
153
+
});
154
+
155
+
let batchResults = await Promise.all(fetchBatch);
156
+
results = results.concat(batchResults);
157
+
}
158
+
159
+
this.updateProgress({ postLikes: 1.0 });
160
+
161
+
return results.flat();
162
+
}
163
+
164
+
/**
165
+
* @typedef {{ handle?: string, did?: string, avatar?: string, count: number }} LikeStat
166
+
* @typedef {Record<string, LikeStat>} LikeStatHash
167
+
*/
168
+
169
+
/** @param {json[]} likes, @returns {LikeStatHash} */
170
+
171
+
sumUpReceivedLikes(likes) {
172
+
/** @type {LikeStatHash} */
173
+
let stats = {};
174
+
175
+
for (let like of likes) {
176
+
let handle = like.actor.handle;
177
+
178
+
if (!stats[handle]) {
179
+
stats[handle] = { handle: handle, count: 0, avatar: like.actor.avatar };
180
+
}
181
+
182
+
stats[handle].count += 1;
183
+
}
184
+
185
+
return stats;
186
+
}
187
+
188
+
/** @param {json[]} likes, @returns {LikeStatHash} */
189
+
190
+
sumUpGivenLikes(likes) {
191
+
/** @type {LikeStatHash} */
192
+
let stats = {};
193
+
194
+
for (let like of likes) {
195
+
let did = atURI(like.value.subject.uri).repo;
196
+
197
+
if (!stats[did]) {
198
+
stats[did] = { did: did, count: 0 };
199
+
}
200
+
201
+
stats[did].count += 1;
202
+
}
203
+
204
+
return stats;
205
+
}
206
+
207
+
/** @param {LikeStatHash} counts, @returns {LikeStat[]} */
208
+
209
+
getTopEntries(counts) {
210
+
return Object.entries(counts).sort(this.sortResults).map(x => x[1]).slice(0, 20);
211
+
}
212
+
213
+
/** @param {LikeStat[]} topUsers, @param {HTMLTableElement} table, @returns {Promise<void>} */
214
+
215
+
async renderResults(topUsers, table) {
216
+
let tableBody = $(table.querySelector('tbody'));
217
+
tableBody.innerHTML = '';
218
+
219
+
for (let [i, user] of topUsers.entries()) {
220
+
let tr = $tag('tr');
221
+
tr.append(
222
+
$tag('td.no', { text: i + 1 }),
223
+
$tag('td.handle', {
224
+
html: `<img class="avatar" src="${user.avatar}"> ` +
225
+
`<a href="https://bsky.app/profile/${user.handle}" target="_blank">${user.handle}</a>`
226
+
}),
227
+
$tag('td.count', { text: user.count })
228
+
);
229
+
230
+
tableBody.append(tr);
231
+
};
232
+
}
233
+
234
+
resetProgress() {
235
+
this.progressBar.value = 0;
236
+
this.progressPosts = 0;
237
+
this.progressLikeRecords = 0;
238
+
this.progressPostLikes = 0;
239
+
}
240
+
241
+
/** @param {{ posts?: number, likeRecords?: number, postLikes?: number }} data */
242
+
243
+
updateProgress(data) {
244
+
if (data.posts) {
245
+
this.progressPosts = data.posts;
246
+
}
247
+
248
+
if (data.likeRecords) {
249
+
this.progressLikeRecords = data.likeRecords;
250
+
}
251
+
252
+
if (data.postLikes) {
253
+
this.progressPostLikes = data.postLikes;
254
+
}
255
+
256
+
let totalProgress = (
257
+
0.1 * this.progressPosts +
258
+
0.65 * this.progressLikeRecords +
259
+
0.25 * this.progressPostLikes
260
+
);
261
+
262
+
this.progressBar.value = totalProgress;
263
+
}
264
+
265
+
/** @param {[string, LikeStat]} a, @param {[string, LikeStat]} b, @returns {-1|1|0} */
266
+
267
+
sortResults(a, b) {
268
+
if (a[1].count < b[1].count) {
269
+
return 1;
270
+
} else if (a[1].count > b[1].count) {
271
+
return -1;
272
+
} else {
273
+
return 0;
274
+
}
275
+
}
276
+
277
+
stopScan() {
278
+
this.submitButton.value = 'Start scan';
279
+
this.progressBar.style.display = 'none';
280
+
this.scanStartTime = undefined;
281
+
}
282
+
}
+3
-1
minisky.js
+3
-1
minisky.js
···
180
}
181
182
/**
183
+
* @typedef {(obj: json[]) => { cancel: true } | void} FetchAllOnPageLoad
184
+
*
185
* @typedef {MiniskyOptions & {
186
* field: string,
187
* breakWhen?: (obj: json) => boolean,
188
+
* onPageLoad?: FetchAllOnPageLoad | undefined
189
* }} FetchAllOptions
190
*
191
* @param {string} method
+17
-135
skythread.js
+17
-135
skythread.js
···
1
function init() {
2
-
let html = $(document.body.parentNode);
3
-
4
window.dateLocale = localStorage.getItem('locale') || undefined;
5
window.isIncognito = !!localStorage.getItem('incognito');
6
window.biohazardEnabled = JSON.parse(localStorage.getItem('biohazard') ?? 'null');
7
8
window.loginDialog = $(document.querySelector('#login'));
9
-
window.accountMenu = $(document.querySelector('#account_menu'));
10
11
window.avatarPreloader = buildAvatarPreloader();
12
13
window.threadPage = new ThreadPage();
14
window.postingStatsPage = new PostingStatsPage();
15
-
16
-
html.addEventListener('click', (e) => {
17
-
$id('account_menu').style.visibility = 'hidden';
18
-
});
19
20
$(document.querySelector('#search form')).addEventListener('submit', (e) => {
21
e.preventDefault();
···
67
68
window.biohazardEnabled = false;
69
localStorage.setItem('biohazard', 'false');
70
-
toggleMenuButton('biohazard', false);
71
72
for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) {
73
$(p).style.display = 'none';
···
78
hideDialog(target.closest('.dialog'));
79
});
80
81
-
$(document.querySelector('#account')).addEventListener('click', (e) => {
82
-
toggleAccountMenu();
83
-
e.stopPropagation();
84
-
});
85
-
86
-
accountMenu.addEventListener('click', (e) => {
87
-
e.stopPropagation();
88
-
});
89
-
90
-
$(accountMenu.querySelector('a[data-action=biohazard]')).addEventListener('click', (e) => {
91
-
e.preventDefault();
92
-
93
-
let hazards = document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post');
94
-
95
-
if (window.biohazardEnabled === false) {
96
-
window.biohazardEnabled = true;
97
-
localStorage.setItem('biohazard', 'true');
98
-
toggleMenuButton('biohazard', true);
99
-
Array.from(hazards).forEach(p => { $(p).style.display = 'block' });
100
-
} else {
101
-
window.biohazardEnabled = false;
102
-
localStorage.setItem('biohazard', 'false');
103
-
toggleMenuButton('biohazard', false);
104
-
Array.from(hazards).forEach(p => { $(p).style.display = 'none' });
105
-
}
106
-
});
107
-
108
-
$(accountMenu.querySelector('a[data-action=incognito]')).addEventListener('click', (e) => {
109
-
e.preventDefault();
110
-
111
-
if (isIncognito) {
112
-
localStorage.removeItem('incognito');
113
-
} else {
114
-
localStorage.setItem('incognito', '1');
115
-
}
116
-
117
-
location.reload();
118
-
});
119
-
120
-
$(accountMenu.querySelector('a[data-action=login]')).addEventListener('click', (e) => {
121
-
e.preventDefault();
122
-
toggleDialog(loginDialog);
123
-
$id('account_menu').style.visibility = 'hidden';
124
-
});
125
-
126
-
$(accountMenu.querySelector('a[data-action=logout]')).addEventListener('click', (e) => {
127
-
e.preventDefault();
128
-
logOut();
129
-
});
130
-
131
window.appView = new BlueskyAPI('api.bsky.app', false);
132
window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false);
133
window.accountAPI = new BlueskyAPI(undefined, true);
134
135
if (accountAPI.isLoggedIn) {
136
accountAPI.host = accountAPI.user.pdsEndpoint;
137
-
hideMenuButton('login');
138
139
if (!isIncognito) {
140
window.api = accountAPI;
141
-
showLoggedInStatus(true, api.user.avatar);
142
} else {
143
window.api = appView;
144
-
showLoggedInStatus('incognito');
145
-
toggleMenuButton('incognito', true);
146
}
147
} else {
148
window.api = appView;
149
-
hideMenuButton('logout');
150
-
hideMenuButton('incognito');
151
}
152
153
-
toggleMenuButton('biohazard', window.biohazardEnabled !== false);
154
155
parseQueryParams();
156
}
···
248
$id('login').classList.toggle('expanded');
249
}
250
251
-
function toggleAccountMenu() {
252
-
let menu = $id('account_menu');
253
-
menu.style.visibility = (menu.style.visibility == 'visible') ? 'hidden' : 'visible';
254
-
}
255
-
256
-
/** @param {string} buttonName */
257
-
258
-
function showMenuButton(buttonName) {
259
-
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
260
-
let item = $(button.parentNode);
261
-
item.style.display = 'list-item';
262
-
}
263
-
264
-
/** @param {string} buttonName */
265
-
266
-
function hideMenuButton(buttonName) {
267
-
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
268
-
let item = $(button.parentNode);
269
-
item.style.display = 'none';
270
-
}
271
-
272
-
/** @param {string} buttonName, @param {boolean} state */
273
-
274
-
function toggleMenuButton(buttonName, state) {
275
-
let button = $(accountMenu.querySelector(`a[data-action=${buttonName}]`));
276
-
let check = $(button.querySelector('.check'));
277
-
check.style.display = (state) ? 'inline' : 'none';
278
-
}
279
-
280
-
/** @param {boolean | 'incognito'} loggedIn, @param {string | undefined | null} [avatar] */
281
-
282
-
function showLoggedInStatus(loggedIn, avatar) {
283
-
let account = $id('account');
284
-
285
-
if (loggedIn === true && avatar) {
286
-
let button = $(account.querySelector('i'));
287
-
288
-
let img = $tag('img.avatar', { src: avatar });
289
-
img.style.display = 'none';
290
-
img.addEventListener('load', () => {
291
-
button.remove();
292
-
img.style.display = 'inline';
293
-
});
294
-
img.addEventListener('error', () => {
295
-
showLoggedInStatus(true, null);
296
-
})
297
-
298
-
account.append(img);
299
-
} else if (loggedIn === false) {
300
-
$id('account').innerHTML = `<i class="fa-regular fa-user-circle fa-xl"></i>`;
301
-
} else if (loggedIn === 'incognito') {
302
-
$id('account').innerHTML = `<i class="fa-solid fa-user-secret fa-lg"></i>`;
303
-
} else {
304
-
account.innerHTML = `<i class="fa-solid fa-user-circle fa-xl"></i>`;
305
-
}
306
-
}
307
-
308
function submitLogin() {
309
let handle = $id('login_handle', HTMLInputElement);
310
let password = $id('login_password', HTMLInputElement);
···
327
submit.style.display = 'inline';
328
cloudy.style.display = 'none';
329
330
-
loadCurrentUserAvatar();
331
-
showMenuButton('logout');
332
-
showMenuButton('incognito');
333
-
hideMenuButton('login');
334
335
let params = new URLSearchParams(location.search);
336
let page = params.get('page');
···
373
return pds;
374
}
375
376
-
function loadCurrentUserAvatar() {
377
-
api.loadCurrentUserAvatar().then((url) => {
378
-
showLoggedInStatus(true, url);
379
-
}).catch((error) => {
380
-
console.log(error);
381
-
showLoggedInStatus(true, null);
382
-
});
383
-
}
384
-
385
function logOut() {
386
accountAPI.resetTokens();
387
localStorage.removeItem('incognito');
···
431
showNotificationsPage();
432
} else if (page == 'posting_stats') {
433
window.postingStatsPage.show();
434
}
435
}
436
···
1
function init() {
2
window.dateLocale = localStorage.getItem('locale') || undefined;
3
window.isIncognito = !!localStorage.getItem('incognito');
4
window.biohazardEnabled = JSON.parse(localStorage.getItem('biohazard') ?? 'null');
5
6
window.loginDialog = $(document.querySelector('#login'));
7
8
window.avatarPreloader = buildAvatarPreloader();
9
10
+
window.accountMenu = new Menu();
11
window.threadPage = new ThreadPage();
12
window.postingStatsPage = new PostingStatsPage();
13
+
window.likeStatsPage = new LikeStatsPage();
14
15
$(document.querySelector('#search form')).addEventListener('submit', (e) => {
16
e.preventDefault();
···
62
63
window.biohazardEnabled = false;
64
localStorage.setItem('biohazard', 'false');
65
+
accountMenu.toggleMenuButtonCheck('biohazard', false);
66
67
for (let p of document.querySelectorAll('p.hidden-replies, .content > .post.blocked, .blocked > .load-post')) {
68
$(p).style.display = 'none';
···
73
hideDialog(target.closest('.dialog'));
74
});
75
76
window.appView = new BlueskyAPI('api.bsky.app', false);
77
window.blueAPI = new BlueskyAPI('blue.mackuba.eu', false);
78
window.accountAPI = new BlueskyAPI(undefined, true);
79
80
if (accountAPI.isLoggedIn) {
81
accountAPI.host = accountAPI.user.pdsEndpoint;
82
+
accountMenu.hideMenuButton('login');
83
84
if (!isIncognito) {
85
window.api = accountAPI;
86
+
accountMenu.showLoggedInStatus(true, api.user.avatar);
87
} else {
88
window.api = appView;
89
+
accountMenu.showLoggedInStatus('incognito');
90
+
accountMenu.toggleMenuButtonCheck('incognito', true);
91
}
92
} else {
93
window.api = appView;
94
+
accountMenu.hideMenuButton('logout');
95
+
accountMenu.hideMenuButton('incognito');
96
}
97
98
+
accountMenu.toggleMenuButtonCheck('biohazard', window.biohazardEnabled !== false);
99
100
parseQueryParams();
101
}
···
193
$id('login').classList.toggle('expanded');
194
}
195
196
function submitLogin() {
197
let handle = $id('login_handle', HTMLInputElement);
198
let password = $id('login_password', HTMLInputElement);
···
215
submit.style.display = 'inline';
216
cloudy.style.display = 'none';
217
218
+
accountMenu.loadCurrentUserAvatar();
219
+
220
+
accountMenu.showMenuButton('logout');
221
+
accountMenu.showMenuButton('incognito');
222
+
accountMenu.hideMenuButton('login');
223
224
let params = new URLSearchParams(location.search);
225
let page = params.get('page');
···
262
return pds;
263
}
264
265
function logOut() {
266
accountAPI.resetTokens();
267
localStorage.removeItem('incognito');
···
311
showNotificationsPage();
312
} else if (page == 'posting_stats') {
313
window.postingStatsPage.show();
314
+
} else if (page == 'like_stats') {
315
+
window.likeStatsPage.show();
316
}
317
}
318
+76
-1
style.css
+76
-1
style.css
···
783
784
#posting_stats_page .scan-result td, #posting_stats_page .scan-result th {
785
border: 1px solid #333;
786
-
padding: 5px 8px;
787
}
788
789
#posting_stats_page .scan-result td {
790
text-align: right;
791
}
792
793
#posting_stats_page .scan-result th {
···
829
830
#posting_stats_page .scan-result td.percent {
831
min-width: 70px;
832
}
833
834
@media (prefers-color-scheme: dark) {
···
1013
1014
#posting_stats_page .scan-result tr.total td {
1015
background-color: hsla(207, 90%, 25%, 0.4);
1016
}
1017
}
···
783
784
#posting_stats_page .scan-result td, #posting_stats_page .scan-result th {
785
border: 1px solid #333;
786
}
787
788
#posting_stats_page .scan-result td {
789
text-align: right;
790
+
padding: 5px 8px;
791
}
792
793
#posting_stats_page .scan-result th {
···
829
830
#posting_stats_page .scan-result td.percent {
831
min-width: 70px;
832
+
}
833
+
834
+
#like_stats_page {
835
+
display: none;
836
+
}
837
+
838
+
#like_stats_page input[type="range"] {
839
+
width: 250px;
840
+
vertical-align: middle;
841
+
}
842
+
843
+
#like_stats_page input[type="submit"] {
844
+
font-size: 12pt;
845
+
margin: 5px 0px;
846
+
padding: 5px 10px;
847
+
}
848
+
849
+
#like_stats_page progress {
850
+
width: 300px;
851
+
margin-left: 10px;
852
+
vertical-align: middle;
853
+
display: none;
854
+
}
855
+
856
+
#like_stats_page .scan-result {
857
+
border: 1px solid #333;
858
+
border-collapse: collapse;
859
+
display: none;
860
+
float: left;
861
+
margin-top: 20px;
862
+
margin-bottom: 40px;
863
+
}
864
+
865
+
#like_stats_page .given-likes {
866
+
margin-right: 100px;
867
+
}
868
+
869
+
#like_stats_page .scan-result td, #like_stats_page .scan-result th {
870
+
border: 1px solid #333;
871
+
padding: 5px 10px;
872
+
}
873
+
874
+
#like_stats_page .scan-result th {
875
+
text-align: center;
876
+
background-color: hsl(207, 100%, 86%);
877
+
padding: 12px 10px;
878
+
}
879
+
880
+
#like_stats_page .scan-result td.no {
881
+
font-weight: bold;
882
+
text-align: right;
883
+
}
884
+
885
+
#like_stats_page .scan-result td.handle {
886
+
width: 280px;
887
+
}
888
+
889
+
#like_stats_page .scan-result td.count {
890
+
padding: 5px 15px;
891
+
}
892
+
893
+
#like_stats_page .scan-result .avatar {
894
+
width: 24px;
895
+
border-radius: 14px;
896
+
vertical-align: middle;
897
+
margin-right: 2px;
898
+
padding: 2px;
899
}
900
901
@media (prefers-color-scheme: dark) {
···
1080
1081
#posting_stats_page .scan-result tr.total td {
1082
background-color: hsla(207, 90%, 25%, 0.4);
1083
+
}
1084
+
1085
+
#like_stats_page .scan-result, #like_stats_page .scan-result td, #like_stats_page .scan-result th {
1086
+
border-color: #888;
1087
+
}
1088
+
1089
+
#like_stats_page .scan-result th {
1090
+
background-color: hsl(207, 90%, 25%);
1091
}
1092
}
+2
-1
types.d.ts
+2
-1
types.d.ts
···
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
declare var loginDialog: HTMLElement;
15
-
declare var accountMenu: HTMLElement;
16
declare var avatarPreloader: IntersectionObserver;
17
declare var threadPage: ThreadPage;
18
declare var postingStatsPage: PostingStatsPage;
19
20
type json = Record<string, any>;
21
···
12
declare var isIncognito: boolean;
13
declare var biohazardEnabled: boolean;
14
declare var loginDialog: HTMLElement;
15
+
declare var accountMenu: Menu;
16
declare var avatarPreloader: IntersectionObserver;
17
declare var threadPage: ThreadPage;
18
declare var postingStatsPage: PostingStatsPage;
19
+
declare var likeStatsPage: LikeStatsPage;
20
21
type json = Record<string, any>;
22