@kangsicj/dcinsideenhancer 1.0.2 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +7 -2
- package/README.md +7 -2
- package/dist/DCInsideEnhancer.meta.js +1 -1
- package/dist/DCInsideEnhancer.user.js +1672 -84
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// @name DCInside Enhancer
|
|
3
3
|
// @name:ko 디시 인핸서
|
|
4
4
|
// @namespace https://github.com/kangsicj/DCInsideEnhancer
|
|
5
|
-
// @version 1.
|
|
5
|
+
// @version 1.1.1
|
|
6
6
|
// @description DCInside filter & enhancer — mobile & PC. Block users/words/heads/notices/images & GIFs across posts, infinite list, zoom viewer, theme sync, encrypted multi-device sync.
|
|
7
7
|
// @description:ko 디시인사이드 차단·편의 기능 (모바일·PC) - 유저·단어(제목/본문)·말머리·공지 차단, 같은/비슷한 이미지·움짤 모든 글 차단, 디시콘 제거, 차단 미디어 데이터 절약, 목록 자동 보충·무한 스크롤·뒤로 가기 위치 복원, 스크랩, 이미지 확대뷰 개선, 야간모드 자동 연동, 암호화 기기 간 동기화
|
|
8
8
|
// @author kangsicj
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
// 0. 설정·상수
|
|
76
76
|
// =========================================================
|
|
77
77
|
|
|
78
|
-
const SCRIPT_VERSION = '1.
|
|
78
|
+
const SCRIPT_VERSION = '1.1.1';
|
|
79
79
|
const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
|
|
80
80
|
const DONATE_URL = 'https://fairy.hada.io/@jinny#support';
|
|
81
81
|
|
|
@@ -104,6 +104,11 @@
|
|
|
104
104
|
syncUndo: 'dc_sync_undo',
|
|
105
105
|
syncLog: 'dc_sync_log',
|
|
106
106
|
syncLock: 'dc_sync_lock',
|
|
107
|
+
syncRt: 'dc_sync_rt',
|
|
108
|
+
recent: 'dc_recent_galls',
|
|
109
|
+
recentNative: 'dc_recent_native',
|
|
110
|
+
searchHistory: 'dc_search_history',
|
|
111
|
+
alarmSeen: 'dc_alarm_marks',
|
|
107
112
|
seenVersion: 'dc_seen_version'
|
|
108
113
|
};
|
|
109
114
|
|
|
@@ -212,6 +217,13 @@
|
|
|
212
217
|
hideHomeNewGall: true,
|
|
213
218
|
hideHomeMedia: true,
|
|
214
219
|
hideFooterInfo: true,
|
|
220
|
+
hideAdSlots: true,
|
|
221
|
+
hideNewsRows: true,
|
|
222
|
+
hideCommentAds: true,
|
|
223
|
+
hideSilbeAlarm: true,
|
|
224
|
+
alarmBubble: true,
|
|
225
|
+
alarmKeepPolling: true,
|
|
226
|
+
keyboardShortcuts: true,
|
|
215
227
|
pcFitWidth: true,
|
|
216
228
|
autoHideScrollbar: true,
|
|
217
229
|
scrapIntercept: true,
|
|
@@ -265,6 +277,8 @@
|
|
|
265
277
|
--dc-type-id: #6d538d;
|
|
266
278
|
--dc-shadow: 0 8px 24px rgba(0,0,0,.16);
|
|
267
279
|
--dc-shadow-sm: 0 2px 8px rgba(0,0,0,.12);
|
|
280
|
+
--dc-raised: #ffffff;
|
|
281
|
+
--dc-raised-shadow: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.08);
|
|
268
282
|
--dc-overlay: rgba(0,0,0,.45);
|
|
269
283
|
--dc-ghost-bg: rgba(128,128,128,.12);
|
|
270
284
|
--dc-ghost-border: rgba(128,128,128,.38);
|
|
@@ -290,6 +304,8 @@
|
|
|
290
304
|
--dc-type-id: #9c86b1;
|
|
291
305
|
--dc-shadow: 0 8px 28px rgba(0,0,0,.55);
|
|
292
306
|
--dc-shadow-sm: 0 2px 10px rgba(0,0,0,.45);
|
|
307
|
+
--dc-raised: #383b44;
|
|
308
|
+
--dc-raised-shadow: inset 0 0 0 1px rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.3);
|
|
293
309
|
--dc-overlay: rgba(0,0,0,.6);
|
|
294
310
|
--dc-ghost-bg: rgba(150,150,150,.11);
|
|
295
311
|
--dc-ghost-border: rgba(150,150,150,.30);
|
|
@@ -395,57 +411,77 @@
|
|
|
395
411
|
|
|
396
412
|
html.dc-pc-fit { --dc-fit-gap: clamp(12px, 2vw, 32px); }
|
|
397
413
|
html.dc-pc-fit body,
|
|
398
|
-
html.dc-pc-fit .dcwrap
|
|
399
|
-
html.dc-pc-fit .dcwrap
|
|
400
|
-
html.dc-pc-fit .dcwrap
|
|
401
|
-
html.dc-pc-fit .dcwrap
|
|
402
|
-
html.dc-pc-fit .dcwrap
|
|
403
|
-
html.dc-pc-fit .dcwrap
|
|
404
|
-
html.dc-pc-fit .dcwrap
|
|
414
|
+
html.dc-pc-fit .dcwrap { min-width: 0 !important; }
|
|
415
|
+
html.dc-pc-fit .dcwrap .dchead,
|
|
416
|
+
html.dc-pc-fit .dcwrap .gnb,
|
|
417
|
+
html.dc-pc-fit .dcwrap .gnb_depth2_list,
|
|
418
|
+
html.dc-pc-fit .dcwrap .info_policy,
|
|
419
|
+
html.dc-pc-fit .dcwrap .copyright,
|
|
420
|
+
html.dc-pc-fit .dcwrap .visit_bookmark {
|
|
405
421
|
width: auto !important;
|
|
406
422
|
max-width: none !important;
|
|
407
423
|
margin-left: var(--dc-fit-gap) !important;
|
|
408
424
|
margin-right: var(--dc-fit-gap) !important;
|
|
409
425
|
}
|
|
410
|
-
|
|
411
|
-
html.dc-pc-fit .dcwrap.
|
|
412
|
-
html.dc-pc-fit .dcwrap
|
|
426
|
+
|
|
427
|
+
html.dc-pc-fit .dcwrap .gnb_bar { min-width: 0 !important; width: auto !important; margin: 0 !important; }
|
|
428
|
+
html.dc-pc-fit .dcwrap .gnb_list,
|
|
429
|
+
html.dc-pc-fit .dcwrap .dc_all { width: auto !important; }
|
|
413
430
|
html.dc-pc-fit .dcwrap .wrap_inner { width: auto !important; }
|
|
414
|
-
html.dc-pc-fit .dcwrap
|
|
431
|
+
html.dc-pc-fit .dcwrap #container {
|
|
432
|
+
width: auto !important;
|
|
433
|
+
margin-left: var(--dc-fit-gap) !important;
|
|
434
|
+
margin-right: var(--dc-fit-gap) !important;
|
|
435
|
+
}
|
|
436
|
+
html.dc-pc-fit .dcwrap #container:has(> .left_content),
|
|
437
|
+
html.dc-pc-fit .dcwrap #container > .main_content:has(> .left_content) {
|
|
415
438
|
display: flex;
|
|
416
439
|
flex-wrap: wrap;
|
|
417
440
|
align-items: flex-start;
|
|
418
441
|
gap: 20px;
|
|
419
442
|
width: auto !important;
|
|
420
|
-
margin: 20px var(--dc-fit-gap) 0 !important;
|
|
421
443
|
}
|
|
422
|
-
html.dc-pc-fit .dcwrap
|
|
423
|
-
html.dc-pc-fit .dcwrap
|
|
424
|
-
html.dc-pc-fit .dcwrap.
|
|
444
|
+
html.dc-pc-fit .dcwrap #container:has(> .left_content)::before,
|
|
445
|
+
html.dc-pc-fit .dcwrap #container:has(> .left_content)::after,
|
|
446
|
+
html.dc-pc-fit .dcwrap .main_content:has(> .left_content)::before,
|
|
447
|
+
html.dc-pc-fit .dcwrap .main_content:has(> .left_content)::after { display: none !important; }
|
|
448
|
+
html.dc-pc-fit .dcwrap #container:has(> .left_content) > :not(.left_content):not(.right_content),
|
|
449
|
+
html.dc-pc-fit .dcwrap .main_content:has(> .left_content) > :not(.left_content):not(.right_content) {
|
|
425
450
|
flex: 0 0 100%;
|
|
426
451
|
float: none !important;
|
|
427
452
|
}
|
|
428
|
-
html.dc-pc-fit .dcwrap
|
|
453
|
+
html.dc-pc-fit .dcwrap :is(#container, .main_content) > .left_content {
|
|
429
454
|
float: none !important;
|
|
430
455
|
flex: 1 1 0;
|
|
431
456
|
width: auto !important;
|
|
432
457
|
min-width: 0;
|
|
433
458
|
}
|
|
434
|
-
html.dc-pc-fit .dcwrap
|
|
459
|
+
html.dc-pc-fit .dcwrap :is(#container, .main_content) > .right_content {
|
|
435
460
|
float: none !important;
|
|
436
461
|
flex: 0 0 300px;
|
|
462
|
+
margin-left: 0 !important;
|
|
437
463
|
}
|
|
438
|
-
html.dc-pc-fit .dcwrap
|
|
439
|
-
html.dc-pc-fit .dcwrap
|
|
440
|
-
html.dc-pc-fit .dcwrap
|
|
441
|
-
html.dc-pc-fit .dcwrap
|
|
464
|
+
html.dc-pc-fit .dcwrap .issuebox,
|
|
465
|
+
html.dc-pc-fit .dcwrap .newvisit_history,
|
|
466
|
+
html.dc-pc-fit .dcwrap .trendalign { width: auto !important; }
|
|
467
|
+
html.dc-pc-fit .dcwrap .list_array_option { width: 100% !important; }
|
|
442
468
|
html.dc-pc-fit .dcwrap .stickyunit { display: none !important; }
|
|
443
469
|
html.dc-pc-fit .dcwrap .darkmodebox .pop_tipbox { left: auto !important; right: 0 !important; }
|
|
444
470
|
|
|
445
471
|
@media (max-width: 1240px) {
|
|
446
|
-
html.dc-pc-fit .dcwrap
|
|
472
|
+
html.dc-pc-fit .dcwrap :is(#container, .main_content) > .right_content { display: none !important; }
|
|
447
473
|
}
|
|
448
474
|
|
|
475
|
+
html.dc-footer-info-off .dcwrap footer.dcfoot,
|
|
476
|
+
html.dc-footer-info-off .dcwrap #container > .ad_bottom_list { display: none !important; }
|
|
477
|
+
|
|
478
|
+
html.dc-ad-slots-off .dcwrap :is(.stickyunit, .rightbanner, .rightbanner1, .banner_box, .ad_bottom_list, #ad_floating, .kakao_ad_area, .ad_top, .view_ad_wrap, .power_link, .con_banner.writing_banbox, #ad_nv_slot, ins.adsbygoogle),
|
|
479
|
+
html.dc-ad-slots-off :is(.adv-group, li.adv-inner, section.grid.clkhigh, #home-ad, .kakao_ad_area) { display: none !important; }
|
|
480
|
+
html.dc-news-rows-off :is(table.gall_list tr.ub-content[data-type="icon_fnews"], .gall-detail-lst > li:has(.sp-lst-fnews)) { display: none !important; }
|
|
481
|
+
html.dc-comment-ads-off :is(.comment_box li.ub-content.dory, .comment_box li.cmt_pw_ad, .all-comment-lst > li.dc-comment-boy) { display: none !important; }
|
|
482
|
+
html.dc-silbe-alarm-off :is(.alarm-popup.silbe-alarm, .rtbAlarmPopup) { display: none !important; }
|
|
483
|
+
html.dc-alarm-bubble :is(.alarm-popup.alarm-default, .alarmPopup) { display: none !important; }
|
|
484
|
+
|
|
449
485
|
html.dc-top-concept-off:not(.dc-peek) .detail-top-concept,
|
|
450
486
|
html.dc-top-concept-off:not(.dc-peek) .con-child-box#recom_box2,
|
|
451
487
|
html:not(.dc-peek) .detail-top-lst.dc-top-empty {
|
|
@@ -919,6 +955,10 @@
|
|
|
919
955
|
|
|
920
956
|
#dc-custom-modal .dc-head-btn {
|
|
921
957
|
flex: 0 0 auto;
|
|
958
|
+
display: inline-flex;
|
|
959
|
+
align-items: center;
|
|
960
|
+
justify-content: center;
|
|
961
|
+
box-sizing: border-box;
|
|
922
962
|
height: 30px;
|
|
923
963
|
padding: 0 12px;
|
|
924
964
|
border: 1px solid var(--dc-border);
|
|
@@ -927,7 +967,7 @@
|
|
|
927
967
|
color: var(--dc-text-dim);
|
|
928
968
|
font-size: 12px;
|
|
929
969
|
font-weight: 700;
|
|
930
|
-
line-height:
|
|
970
|
+
line-height: 1;
|
|
931
971
|
cursor: pointer;
|
|
932
972
|
}
|
|
933
973
|
|
|
@@ -939,7 +979,7 @@
|
|
|
939
979
|
|
|
940
980
|
#dc-custom-modal .dc-modal-tabs {
|
|
941
981
|
display: flex;
|
|
942
|
-
gap:
|
|
982
|
+
gap: 2px;
|
|
943
983
|
margin: 0;
|
|
944
984
|
padding: 3px;
|
|
945
985
|
border-radius: 12px;
|
|
@@ -948,24 +988,36 @@
|
|
|
948
988
|
|
|
949
989
|
#dc-custom-modal .dc-modal-tab {
|
|
950
990
|
flex: 1 1 0;
|
|
991
|
+
display: flex;
|
|
992
|
+
align-items: center;
|
|
993
|
+
justify-content: center;
|
|
994
|
+
gap: 3px;
|
|
995
|
+
min-width: 0;
|
|
996
|
+
box-sizing: border-box;
|
|
951
997
|
height: 36px;
|
|
998
|
+
margin: 0;
|
|
999
|
+
line-height: 1;
|
|
952
1000
|
padding: 0 2px;
|
|
953
1001
|
border: none;
|
|
954
1002
|
border-radius: 9px;
|
|
955
1003
|
background: transparent;
|
|
956
1004
|
color: var(--dc-text-dim);
|
|
957
|
-
font-size:
|
|
1005
|
+
font-size: 12px;
|
|
958
1006
|
font-weight: 700;
|
|
1007
|
+
letter-spacing: -.3px;
|
|
959
1008
|
white-space: nowrap;
|
|
960
1009
|
cursor: pointer;
|
|
961
1010
|
}
|
|
962
1011
|
|
|
963
1012
|
#dc-custom-modal .dc-modal-tab.active {
|
|
964
|
-
background: var(--dc-
|
|
1013
|
+
background: var(--dc-raised);
|
|
965
1014
|
color: var(--dc-accent-ink);
|
|
966
|
-
box-shadow: var(--dc-shadow
|
|
1015
|
+
box-shadow: var(--dc-raised-shadow);
|
|
967
1016
|
}
|
|
968
1017
|
|
|
1018
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-ico { flex: 0 0 auto; font-size: 13px; line-height: 1; }
|
|
1019
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
|
|
1020
|
+
|
|
969
1021
|
#dc-custom-modal .dc-section-title {
|
|
970
1022
|
display: flex;
|
|
971
1023
|
align-items: center;
|
|
@@ -1678,6 +1730,129 @@
|
|
|
1678
1730
|
|
|
1679
1731
|
@keyframes dc-pop { from { transform: scale(0); } to { transform: scale(1); } }
|
|
1680
1732
|
|
|
1733
|
+
#dc-custom-fab[data-dc-alarm]::before {
|
|
1734
|
+
content: attr(data-dc-alarm);
|
|
1735
|
+
position: absolute;
|
|
1736
|
+
top: -6px;
|
|
1737
|
+
left: -6px;
|
|
1738
|
+
min-width: 18px;
|
|
1739
|
+
height: 18px;
|
|
1740
|
+
padding: 0 5px;
|
|
1741
|
+
box-sizing: border-box;
|
|
1742
|
+
border: 2px solid var(--dc-surface);
|
|
1743
|
+
border-radius: 9px;
|
|
1744
|
+
background: var(--dc-danger);
|
|
1745
|
+
color: #fff;
|
|
1746
|
+
font-size: 10.5px;
|
|
1747
|
+
font-weight: 800;
|
|
1748
|
+
line-height: 14px;
|
|
1749
|
+
text-align: center;
|
|
1750
|
+
pointer-events: none;
|
|
1751
|
+
animation: dc-pop .35s ease-out;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
#dc-custom-fab[data-dc-alarm] { opacity: 1; }
|
|
1755
|
+
|
|
1756
|
+
#dc-bubbles {
|
|
1757
|
+
position: fixed;
|
|
1758
|
+
right: 15px;
|
|
1759
|
+
bottom: calc(144px + env(safe-area-inset-bottom));
|
|
1760
|
+
z-index: 99998;
|
|
1761
|
+
display: flex;
|
|
1762
|
+
flex-direction: column;
|
|
1763
|
+
align-items: flex-end;
|
|
1764
|
+
gap: 8px;
|
|
1765
|
+
width: min(320px, calc(100vw - 24px));
|
|
1766
|
+
pointer-events: none;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
#dc-bubbles.dc-left { align-items: flex-start; }
|
|
1770
|
+
|
|
1771
|
+
#dc-bubbles .dc-bubble {
|
|
1772
|
+
position: relative;
|
|
1773
|
+
box-sizing: border-box;
|
|
1774
|
+
width: 100%;
|
|
1775
|
+
padding: 10px 12px 11px;
|
|
1776
|
+
border: 1px solid var(--dc-border);
|
|
1777
|
+
border-radius: 16px 16px 6px 16px;
|
|
1778
|
+
background: var(--dc-surface);
|
|
1779
|
+
color: var(--dc-text);
|
|
1780
|
+
box-shadow: var(--dc-shadow);
|
|
1781
|
+
font-size: 13px;
|
|
1782
|
+
line-height: 1.4;
|
|
1783
|
+
cursor: pointer;
|
|
1784
|
+
pointer-events: auto;
|
|
1785
|
+
touch-action: pan-y;
|
|
1786
|
+
user-select: none;
|
|
1787
|
+
-webkit-user-select: none;
|
|
1788
|
+
opacity: 0;
|
|
1789
|
+
transform: translateY(10px) scale(.96);
|
|
1790
|
+
transition: opacity .22s ease, transform .22s ease;
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
#dc-bubbles.dc-left .dc-bubble { border-radius: 16px 16px 16px 6px; }
|
|
1794
|
+
#dc-bubbles .dc-bubble.dc-in { opacity: 1; transform: none; }
|
|
1795
|
+
#dc-bubbles .dc-bubble.dc-out { opacity: 0; transform: translateY(6px) scale(.96); pointer-events: none; }
|
|
1796
|
+
|
|
1797
|
+
#dc-bubbles .dc-bubble-top {
|
|
1798
|
+
display: flex;
|
|
1799
|
+
align-items: center;
|
|
1800
|
+
gap: 6px;
|
|
1801
|
+
margin-bottom: 3px;
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
#dc-bubbles .dc-bubble-tag {
|
|
1805
|
+
flex: 0 0 auto;
|
|
1806
|
+
padding: 1px 7px;
|
|
1807
|
+
border-radius: 8px;
|
|
1808
|
+
background: var(--dc-accent-soft);
|
|
1809
|
+
color: var(--dc-accent-ink);
|
|
1810
|
+
font-size: 11px;
|
|
1811
|
+
font-weight: 800;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
#dc-bubbles .dc-bubble-time {
|
|
1815
|
+
flex: 1 1 auto;
|
|
1816
|
+
min-width: 0;
|
|
1817
|
+
color: var(--dc-text-dim);
|
|
1818
|
+
font-size: 11px;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
#dc-bubbles .dc-bubble-x {
|
|
1822
|
+
flex: 0 0 auto;
|
|
1823
|
+
width: 24px;
|
|
1824
|
+
height: 24px;
|
|
1825
|
+
margin: -4px -6px -4px 0;
|
|
1826
|
+
padding: 0;
|
|
1827
|
+
border: none;
|
|
1828
|
+
border-radius: 50%;
|
|
1829
|
+
background: transparent;
|
|
1830
|
+
color: var(--dc-text-dim);
|
|
1831
|
+
font-size: 17px;
|
|
1832
|
+
line-height: 24px;
|
|
1833
|
+
cursor: pointer;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
#dc-bubbles .dc-bubble-text {
|
|
1837
|
+
display: -webkit-box;
|
|
1838
|
+
overflow: hidden;
|
|
1839
|
+
-webkit-line-clamp: 2;
|
|
1840
|
+
-webkit-box-orient: vertical;
|
|
1841
|
+
word-break: break-all;
|
|
1842
|
+
font-weight: 600;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
#dc-bubbles .dc-bubble-more {
|
|
1846
|
+
margin-top: 4px;
|
|
1847
|
+
color: var(--dc-accent-ink);
|
|
1848
|
+
font-size: 11.5px;
|
|
1849
|
+
font-weight: 700;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
#dc-custom-modal .dc-row.dc-row-new .dc-row-title { font-weight: 800; }
|
|
1853
|
+
|
|
1854
|
+
.dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
|
|
1855
|
+
|
|
1681
1856
|
#dc-custom-modal .dc-about {
|
|
1682
1857
|
margin-top: 14px;
|
|
1683
1858
|
padding: 12px 13px;
|
|
@@ -2799,6 +2974,11 @@
|
|
|
2799
2974
|
root.classList.toggle('dc-home-media-off', settings.hideHomeMedia !== false);
|
|
2800
2975
|
root.classList.toggle('dc-footer-info-off', settings.hideFooterInfo !== false);
|
|
2801
2976
|
root.classList.toggle('dc-pc-fit', IS_PC_SITE && settings.pcFitWidth !== false);
|
|
2977
|
+
root.classList.toggle('dc-ad-slots-off', settings.hideAdSlots !== false);
|
|
2978
|
+
root.classList.toggle('dc-news-rows-off', settings.hideNewsRows !== false);
|
|
2979
|
+
root.classList.toggle('dc-comment-ads-off', settings.hideCommentAds !== false);
|
|
2980
|
+
root.classList.toggle('dc-silbe-alarm-off', settings.hideSilbeAlarm !== false);
|
|
2981
|
+
root.classList.toggle('dc-alarm-bubble', settings.alarmBubble !== false);
|
|
2802
2982
|
root.classList.toggle('dc-hide-scrollbar', settings.autoHideScrollbar !== false && root.classList.contains('dc-classic-scrollbar'));
|
|
2803
2983
|
};
|
|
2804
2984
|
|
|
@@ -2953,6 +3133,7 @@
|
|
|
2953
3133
|
until: Number(stored.until) || 0,
|
|
2954
3134
|
last: Number(stored.last) || 0,
|
|
2955
3135
|
strikes: Number(stored.strikes) || 0,
|
|
3136
|
+
src: stored.src === 'page' ? 'page' : 'fetch',
|
|
2956
3137
|
lastStrike: Number(stored.lastStrike) || 0,
|
|
2957
3138
|
hits: Array.isArray(stored.hits) ? stored.hits.filter(t => Date.now() - t < NET_WINDOW) : [],
|
|
2958
3139
|
autoHits: Array.isArray(stored.autoHits) ? stored.autoHits.filter(t => Date.now() - t < NET_WINDOW) : [],
|
|
@@ -2979,10 +3160,12 @@
|
|
|
2979
3160
|
|
|
2980
3161
|
const netListeners = [];
|
|
2981
3162
|
|
|
2982
|
-
const noteNetBlocked = () => {
|
|
3163
|
+
const noteNetBlocked = (source = 'fetch') => {
|
|
2983
3164
|
const state = readNetState();
|
|
2984
3165
|
const now = Date.now();
|
|
2985
3166
|
|
|
3167
|
+
state.src = source;
|
|
3168
|
+
|
|
2986
3169
|
state.strikes = now - state.lastStrike < 60 * 60 * 1000 ? state.strikes + 1 : 1;
|
|
2987
3170
|
state.lastStrike = now;
|
|
2988
3171
|
state.until = now + Math.min(NET_COOLDOWN_MAX, NET_COOLDOWN * 2 ** (state.strikes - 1));
|
|
@@ -3024,10 +3207,21 @@
|
|
|
3024
3207
|
}
|
|
3025
3208
|
};
|
|
3026
3209
|
|
|
3210
|
+
const NET_PROBATION = 60 * 1000;
|
|
3211
|
+
|
|
3027
3212
|
const noteSitePageView = () => {
|
|
3028
3213
|
const state = readNetState();
|
|
3214
|
+
const now = Date.now();
|
|
3215
|
+
|
|
3216
|
+
state.recent.push(now);
|
|
3217
|
+
|
|
3218
|
+
if (state.until > now + NET_PROBATION && state.src !== 'page') {
|
|
3219
|
+
state.until = now + NET_PROBATION;
|
|
3220
|
+
writeNetState(state);
|
|
3221
|
+
scheduleNetResume();
|
|
3222
|
+
return;
|
|
3223
|
+
}
|
|
3029
3224
|
|
|
3030
|
-
state.recent.push(Date.now());
|
|
3031
3225
|
writeNetState(state);
|
|
3032
3226
|
};
|
|
3033
3227
|
|
|
@@ -3041,6 +3235,8 @@
|
|
|
3041
3235
|
let netHiddenAt = 0;
|
|
3042
3236
|
let netResumedAt = 0;
|
|
3043
3237
|
|
|
3238
|
+
let netLeavingAt = 0;
|
|
3239
|
+
|
|
3044
3240
|
const pageIsHidden = () => document.visibilityState === 'hidden';
|
|
3045
3241
|
|
|
3046
3242
|
document.addEventListener('visibilitychange', () => {
|
|
@@ -3052,7 +3248,48 @@
|
|
|
3052
3248
|
});
|
|
3053
3249
|
window.addEventListener('pagehide', () => {
|
|
3054
3250
|
netHiddenAt = Date.now();
|
|
3251
|
+
netLeavingAt = Date.now();
|
|
3252
|
+
});
|
|
3253
|
+
|
|
3254
|
+
const noteNetLeaving = () => {
|
|
3255
|
+
netLeavingAt = Date.now();
|
|
3256
|
+
};
|
|
3257
|
+
|
|
3258
|
+
window.addEventListener('beforeunload', noteNetLeaving);
|
|
3259
|
+
|
|
3260
|
+
window.addEventListener('click', event => {
|
|
3261
|
+
const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
|
|
3262
|
+
|
|
3263
|
+
if (!link || event.defaultPrevented || event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
if (link.target && link.target !== '_self') {
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3271
|
+
try {
|
|
3272
|
+
const to = new URL(link.href, location.href);
|
|
3273
|
+
|
|
3274
|
+
if (/^https?:$/.test(to.protocol) && (to.pathname !== location.pathname || to.search !== location.search)) {
|
|
3275
|
+
noteNetLeaving();
|
|
3276
|
+
}
|
|
3277
|
+
} catch {
|
|
3278
|
+
}
|
|
3055
3279
|
});
|
|
3280
|
+
|
|
3281
|
+
try {
|
|
3282
|
+
if (window.navigation && typeof window.navigation.addEventListener === 'function') {
|
|
3283
|
+
window.navigation.addEventListener('navigate', event => {
|
|
3284
|
+
if (!event.hashChange && !(event.destination && event.destination.sameDocument)) {
|
|
3285
|
+
noteNetLeaving();
|
|
3286
|
+
}
|
|
3287
|
+
});
|
|
3288
|
+
}
|
|
3289
|
+
} catch {
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
const netLeavingRecently = () => netLeavingAt && Date.now() - netLeavingAt < 6000;
|
|
3056
3293
|
window.addEventListener('pageshow', event => {
|
|
3057
3294
|
if (event.persisted) {
|
|
3058
3295
|
netResumedAt = Date.now();
|
|
@@ -3066,7 +3303,18 @@
|
|
|
3066
3303
|
});
|
|
3067
3304
|
|
|
3068
3305
|
const unreliableWindow = startedAt =>
|
|
3069
|
-
pageIsHidden() || netHiddenAt >= startedAt ||
|
|
3306
|
+
pageIsHidden() || netHiddenAt >= startedAt || netLeavingAt >= startedAt || netLeavingRecently() ||
|
|
3307
|
+
Date.now() - netResumedAt < NET_RESUME_SETTLE + 3000;
|
|
3308
|
+
|
|
3309
|
+
const truncatedResponse = (response, text) => {
|
|
3310
|
+
try {
|
|
3311
|
+
const length = Number(response.headers && response.headers.get && response.headers.get('content-length'));
|
|
3312
|
+
|
|
3313
|
+
return Number.isFinite(length) && length > 0 && !text.trim();
|
|
3314
|
+
} catch {
|
|
3315
|
+
return false;
|
|
3316
|
+
}
|
|
3317
|
+
};
|
|
3070
3318
|
|
|
3071
3319
|
class NetTransientError extends Error {}
|
|
3072
3320
|
|
|
@@ -3084,7 +3332,7 @@
|
|
|
3084
3332
|
throw new NetPausedError('paused');
|
|
3085
3333
|
}
|
|
3086
3334
|
|
|
3087
|
-
if (pageIsHidden()) {
|
|
3335
|
+
if (pageIsHidden() || netLeavingRecently()) {
|
|
3088
3336
|
throw new NetTransientError('hidden');
|
|
3089
3337
|
}
|
|
3090
3338
|
|
|
@@ -3158,7 +3406,7 @@
|
|
|
3158
3406
|
let { response, text } = await fetchOnce('');
|
|
3159
3407
|
|
|
3160
3408
|
if (looksBlockedResponse(response, text)) {
|
|
3161
|
-
if (unreliableWindow(startedAt)) {
|
|
3409
|
+
if (unreliableWindow(startedAt) || truncatedResponse(response, text)) {
|
|
3162
3410
|
throw new NetTransientError('unreliable');
|
|
3163
3411
|
}
|
|
3164
3412
|
|
|
@@ -3170,11 +3418,17 @@
|
|
|
3170
3418
|
({ response, text } = await fetchOnce(options.cache === 'no-store' ? 'no-store' : 'reload'));
|
|
3171
3419
|
|
|
3172
3420
|
if (looksBlockedResponse(response, text)) {
|
|
3421
|
+
if (unreliableWindow(startedAt) || truncatedResponse(response, text)) {
|
|
3422
|
+
throw new NetTransientError('unreliable');
|
|
3423
|
+
}
|
|
3424
|
+
|
|
3425
|
+
await sleepMs(1500 * NET_GAP_SCALE);
|
|
3426
|
+
|
|
3173
3427
|
if (unreliableWindow(startedAt)) {
|
|
3174
3428
|
throw new NetTransientError('unreliable');
|
|
3175
3429
|
}
|
|
3176
3430
|
|
|
3177
|
-
noteNetBlocked();
|
|
3431
|
+
noteNetBlocked('fetch');
|
|
3178
3432
|
throw new NetPausedError('blocked');
|
|
3179
3433
|
}
|
|
3180
3434
|
}
|
|
@@ -5592,6 +5846,38 @@
|
|
|
5592
5846
|
// =========================================================
|
|
5593
5847
|
|
|
5594
5848
|
let toastTimer = null;
|
|
5849
|
+
const toastListeners = [];
|
|
5850
|
+
|
|
5851
|
+
const notifyToastListeners = () => toastListeners.forEach(fn => {
|
|
5852
|
+
try {
|
|
5853
|
+
fn();
|
|
5854
|
+
} catch {
|
|
5855
|
+
}
|
|
5856
|
+
});
|
|
5857
|
+
|
|
5858
|
+
const placeToast = toast => {
|
|
5859
|
+
toast.style.bottom = '';
|
|
5860
|
+
|
|
5861
|
+
const fab = document.getElementById('dc-custom-fab');
|
|
5862
|
+
|
|
5863
|
+
if (!fab || getComputedStyle(fab).display === 'none') {
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
|
|
5867
|
+
const f = fab.getBoundingClientRect();
|
|
5868
|
+
const t = toast.getBoundingClientRect();
|
|
5869
|
+
|
|
5870
|
+
if (!f.width || !t.width) {
|
|
5871
|
+
return;
|
|
5872
|
+
}
|
|
5873
|
+
|
|
5874
|
+
const overlapX = t.left < f.right + 6 && t.right > f.left - 6;
|
|
5875
|
+
const overlapY = t.top < f.bottom + 6 && t.bottom > f.top - 6;
|
|
5876
|
+
|
|
5877
|
+
if (overlapX && overlapY) {
|
|
5878
|
+
toast.style.bottom = `${Math.round(window.innerHeight - f.top + 10)}px`;
|
|
5879
|
+
}
|
|
5880
|
+
};
|
|
5595
5881
|
|
|
5596
5882
|
const showToast = (text, { actionLabel = '', onAction = null, duration = 3500 } = {}) => {
|
|
5597
5883
|
let toast = document.getElementById('dc-toast');
|
|
@@ -5620,8 +5906,13 @@
|
|
|
5620
5906
|
};
|
|
5621
5907
|
|
|
5622
5908
|
toast.classList.add('show');
|
|
5909
|
+
placeToast(toast);
|
|
5910
|
+
notifyToastListeners();
|
|
5623
5911
|
clearTimeout(toastTimer);
|
|
5624
|
-
toastTimer = setTimeout(() =>
|
|
5912
|
+
toastTimer = setTimeout(() => {
|
|
5913
|
+
toast.classList.remove('show');
|
|
5914
|
+
setTimeout(notifyToastListeners, 220);
|
|
5915
|
+
}, duration);
|
|
5625
5916
|
};
|
|
5626
5917
|
|
|
5627
5918
|
const offerUndo = (text, undo) => {
|
|
@@ -6814,6 +7105,8 @@
|
|
|
6814
7105
|
document.querySelectorAll(COMMENT_ITEM_SELECTOR).forEach(li => {
|
|
6815
7106
|
processUserNode(li, li.querySelector(COMMENT_NICK_SELECTOR), 'comment');
|
|
6816
7107
|
li.classList.toggle('dc-comment-hidden', settings.hideDcconComments !== false && isDcconOnlyComment(li));
|
|
7108
|
+
|
|
7109
|
+
li.classList.toggle('dc-comment-boy', /^\s*댓글돌이\s*$/.test((li.querySelector('.nick') || {}).textContent || ''));
|
|
6817
7110
|
});
|
|
6818
7111
|
|
|
6819
7112
|
processMediaElements();
|
|
@@ -9700,6 +9993,7 @@
|
|
|
9700
9993
|
const SETTING_GROUPS = [
|
|
9701
9994
|
{ key: 'block', title: '🚫 차단 방식', intro: '차단한 글·댓글을 화면에서 어떻게 처리할지 정합니다.' },
|
|
9702
9995
|
{ key: 'clean', title: '🧹 화면 정리', intro: '사이트가 기본으로 보여 주는 영역 중 필요 없는 것을 숨깁니다.' },
|
|
9996
|
+
{ key: 'alarm', title: '🔔 알림', intro: '디시 알림(댓글·답글·힛갤 등)을 보기 쉽게 바꿉니다. 알림 내용은 사이트가 받은 것을 그대로 씁니다.' },
|
|
9703
9997
|
{ key: 'dccon', title: '😶 디시콘', intro: '디시 자체 이모티콘을 다룹니다.' },
|
|
9704
9998
|
{ key: 'list', title: '📜 목록 보기', intro: '게시판 목록을 넘겨 보는 방식을 정합니다.' },
|
|
9705
9999
|
{ key: 'media', title: '🖼 이미지·영상', intro: '본문 이미지 차단과 확대 보기를 다룹니다.' },
|
|
@@ -9823,22 +10117,71 @@
|
|
|
9823
10117
|
off: '디시미디어·디시이슈 묶음을 보여 줍니다.',
|
|
9824
10118
|
refresh: 'root'
|
|
9825
10119
|
},
|
|
10120
|
+
{
|
|
10121
|
+
group: 'clean',
|
|
10122
|
+
key: 'hideNewsRows',
|
|
10123
|
+
name: '목록의 제휴 뉴스 숨기기',
|
|
10124
|
+
on: "말머리가 '뉴스'인 제휴 기사(광고성 글)를 목록에서 숨깁니다.",
|
|
10125
|
+
off: '제휴 뉴스를 목록에 그대로 보여 줍니다.',
|
|
10126
|
+
refresh: 'root'
|
|
10127
|
+
},
|
|
10128
|
+
{
|
|
10129
|
+
group: 'clean',
|
|
10130
|
+
key: 'hideCommentAds',
|
|
10131
|
+
name: '댓글돌이·파워링크 댓글 숨기기',
|
|
10132
|
+
on: '댓글 사이에 끼어드는 댓글돌이(기사·광고 넘겨 보기)와 파워링크 댓글을 숨깁니다.',
|
|
10133
|
+
off: '댓글돌이를 그대로 보여 줍니다.',
|
|
10134
|
+
refresh: 'root'
|
|
10135
|
+
},
|
|
10136
|
+
{
|
|
10137
|
+
group: 'clean',
|
|
10138
|
+
key: 'hideAdSlots',
|
|
10139
|
+
name: '광고 자리 숨기기',
|
|
10140
|
+
on: '배너·파워링크·구글 광고처럼 광고가 들어가는 칸을 숨겨 빈 공간 없이 보여 줍니다.',
|
|
10141
|
+
off: '광고 칸을 사이트 기본처럼 둡니다.',
|
|
10142
|
+
refresh: 'root'
|
|
10143
|
+
},
|
|
9826
10144
|
{
|
|
9827
10145
|
group: 'clean',
|
|
9828
10146
|
key: 'pcFitWidth',
|
|
9829
10147
|
name: 'PC 화면을 창 크기에 꽉 차게 (PC)',
|
|
9830
|
-
on: '가운데 고정 폭
|
|
10148
|
+
on: '가운데 고정 폭 대신 창 너비에 맞춰 갤러리 목록·글·디시 홈을 넓힙니다. 창이 좁으면 오른쪽 칸(로그인·실시간 베스트)을 숨겨 목록이 잘리지 않게 합니다.',
|
|
9831
10149
|
off: '사이트 기본처럼 가운데 고정 폭으로 보여 줍니다.',
|
|
9832
10150
|
refresh: 'root'
|
|
9833
10151
|
},
|
|
9834
10152
|
{
|
|
9835
10153
|
group: 'clean',
|
|
9836
10154
|
key: 'hideFooterInfo',
|
|
9837
|
-
name: '맨 아래
|
|
9838
|
-
on: '
|
|
10155
|
+
name: '맨 아래 정보 영역 숨기기',
|
|
10156
|
+
on: '모바일은 맨 아래 앱 설치·회사 소개·약관·저작권 줄을, PC는 검색창 아래의 갤러리 분류 목록·야간모드/맨위로·회사 정보·저작권을 숨깁니다. 창이 작아도 목록이 가려지지 않습니다.',
|
|
9839
10157
|
off: '맨 아래 정보 줄을 모두 보여 줍니다.',
|
|
9840
10158
|
refresh: 'root'
|
|
9841
10159
|
},
|
|
10160
|
+
{
|
|
10161
|
+
group: 'clean',
|
|
10162
|
+
key: 'hideSilbeAlarm',
|
|
10163
|
+
name: '실베(실시간 베스트) 알림 팝업 숨기기',
|
|
10164
|
+
on: '내 알림과 상관없이 화면 구석에 뜨는 실시간 베스트 홍보 한 줄 알림을 숨깁니다.',
|
|
10165
|
+
off: '실시간 베스트 알림을 사이트 기본처럼 띄웁니다.',
|
|
10166
|
+
refresh: 'root'
|
|
10167
|
+
},
|
|
10168
|
+
{
|
|
10169
|
+
group: 'alarm',
|
|
10170
|
+
key: 'alarmBubble',
|
|
10171
|
+
name: '알림을 플로팅 말풍선으로',
|
|
10172
|
+
on: '사이트의 한 줄 알림 대신 차단 관리 버튼 곁에 말풍선으로 띄우고, 읽지 않은 알림 수를 버튼에 표시합니다. 누르면 이동, 좌우로 밀면 닫힙니다. 지난 알림은 [🕘 최근] 탭에 모입니다.',
|
|
10173
|
+
off: '사이트 기본 한 줄 알림을 그대로 씁니다.',
|
|
10174
|
+
refresh: 'root'
|
|
10175
|
+
},
|
|
10176
|
+
{
|
|
10177
|
+
group: 'alarm',
|
|
10178
|
+
key: 'alarmKeepPolling',
|
|
10179
|
+
parent: 'alarmBubble',
|
|
10180
|
+
name: '오래 보는 페이지에서도 알림 받기 (모바일)',
|
|
10181
|
+
on: '모바일 사이트는 한 페이지에서 알림을 세 번(약 4분 반)만 확인합니다. 그 뒤에도 화면을 보는 동안 3분마다 이어서 확인합니다.',
|
|
10182
|
+
off: '사이트 기본처럼 페이지를 옮길 때만 다시 확인합니다.',
|
|
10183
|
+
refresh: 'none'
|
|
10184
|
+
},
|
|
9842
10185
|
{
|
|
9843
10186
|
group: 'dccon',
|
|
9844
10187
|
key: 'blockDccon',
|
|
@@ -9855,6 +10198,14 @@
|
|
|
9855
10198
|
off: '모든 댓글을 보여 줍니다.',
|
|
9856
10199
|
refresh: 'all'
|
|
9857
10200
|
},
|
|
10201
|
+
{
|
|
10202
|
+
group: 'list',
|
|
10203
|
+
key: 'keyboardShortcuts',
|
|
10204
|
+
name: '키보드 단축키 (PC)',
|
|
10205
|
+
on: '목록에서 J/K 로 글을 고르고 Enter 로 엽니다. 글에서는 J/K 로 다음·이전 글, S 스크랩, / 검색창, B 차단 관리, ? 안내.',
|
|
10206
|
+
off: '단축키를 쓰지 않습니다.',
|
|
10207
|
+
refresh: 'none'
|
|
10208
|
+
},
|
|
9858
10209
|
{
|
|
9859
10210
|
group: 'list',
|
|
9860
10211
|
key: 'autoFillList',
|
|
@@ -10048,6 +10399,10 @@
|
|
|
10048
10399
|
repinReadColors();
|
|
10049
10400
|
}
|
|
10050
10401
|
|
|
10402
|
+
if (item.key === 'alarmBubble') {
|
|
10403
|
+
paintAlarmBadge();
|
|
10404
|
+
}
|
|
10405
|
+
|
|
10051
10406
|
break;
|
|
10052
10407
|
|
|
10053
10408
|
case 'log':
|
|
@@ -11008,7 +11363,11 @@
|
|
|
11008
11363
|
'<button id="dc-modal-close-btn" class="dc-head-btn" type="button">닫기</button>' +
|
|
11009
11364
|
'</div>' +
|
|
11010
11365
|
`<div class="dc-modal-tabs">${Object.keys(MODAL_VIEWS)
|
|
11011
|
-
.map(key =>
|
|
11366
|
+
.map(key => {
|
|
11367
|
+
const [ico, ...rest] = MODAL_VIEWS[key].tab.split(' ');
|
|
11368
|
+
|
|
11369
|
+
return `<button class="dc-modal-tab" type="button" data-dc-view="${key}"><span class="dc-tab-ico" aria-hidden="true">${ico}</span><span class="dc-tab-txt">${rest.join(' ')}</span></button>`;
|
|
11370
|
+
})
|
|
11012
11371
|
.join('')}</div>` +
|
|
11013
11372
|
'</div>' +
|
|
11014
11373
|
'<div id="dc-modal-content"></div>';
|
|
@@ -11080,6 +11439,9 @@
|
|
|
11080
11439
|
const SYNC_MIN_GAP = 20000;
|
|
11081
11440
|
const SYNC_POLL = 60000;
|
|
11082
11441
|
const SYNC_LOCK_MS = 20000;
|
|
11442
|
+
const SYNC_RETRY = [3000, 10000, 30000];
|
|
11443
|
+
const SYNC_FAIL_SHOW = 3;
|
|
11444
|
+
const SYNC_STUCK_MS = 30000;
|
|
11083
11445
|
const SYNC_DEVICE_REFRESH = 10 * 60 * 1000;
|
|
11084
11446
|
const SYNC_DEVICE_STALE = 60 * 24 * 3600 * 1000;
|
|
11085
11447
|
const SYNC_LOG_MAX = 20;
|
|
@@ -11094,6 +11456,8 @@
|
|
|
11094
11456
|
{ key: 'words', label: '단어', fields: ['keywords', 'titles'] },
|
|
11095
11457
|
{ key: 'heads', label: '말머리', fields: ['heads'] },
|
|
11096
11458
|
{ key: 'scraps', label: '스크랩', fields: ['scraps'] },
|
|
11459
|
+
{ key: 'recent', label: '최근 갤러리', fields: ['recent'] },
|
|
11460
|
+
{ key: 'search', label: '검색 기록', fields: ['search'] },
|
|
11097
11461
|
{ key: 'settings', label: '설정', fields: ['settings'] }
|
|
11098
11462
|
];
|
|
11099
11463
|
|
|
@@ -11260,7 +11624,9 @@
|
|
|
11260
11624
|
titles: [...blockedTitles],
|
|
11261
11625
|
heads: JSON.parse(JSON.stringify(hiddenHeadMap || {})),
|
|
11262
11626
|
scraps: scraps.map(item => ({ ...item })),
|
|
11263
|
-
settings: syncedSettings()
|
|
11627
|
+
settings: syncedSettings(),
|
|
11628
|
+
recent: collectRecentForSync(),
|
|
11629
|
+
search: collectSearchForSync()
|
|
11264
11630
|
});
|
|
11265
11631
|
|
|
11266
11632
|
const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
|
|
@@ -11366,7 +11732,9 @@
|
|
|
11366
11732
|
titles,
|
|
11367
11733
|
heads: mergeHeads(local.heads, base && base.heads, remote && remote.heads),
|
|
11368
11734
|
scraps: mergeScraps(local.scraps, base && base.scraps, remote && remote.scraps),
|
|
11369
|
-
settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings)
|
|
11735
|
+
settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings),
|
|
11736
|
+
recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
|
|
11737
|
+
search: mergeSearchHistory(local.search, base && base.search, remote && remote.search)
|
|
11370
11738
|
};
|
|
11371
11739
|
|
|
11372
11740
|
skippedFields(skip).forEach(field => {
|
|
@@ -11416,9 +11784,20 @@
|
|
|
11416
11784
|
|
|
11417
11785
|
let syncApplying = false;
|
|
11418
11786
|
|
|
11787
|
+
const blockSyncData = () => {
|
|
11788
|
+
const data = collectSyncData();
|
|
11789
|
+
|
|
11790
|
+
delete data.recent;
|
|
11791
|
+
delete data.search;
|
|
11792
|
+
|
|
11793
|
+
return JSON.stringify(data);
|
|
11794
|
+
};
|
|
11795
|
+
|
|
11419
11796
|
const applySyncData = data => {
|
|
11797
|
+
const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
|
|
11798
|
+
const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
|
|
11420
11799
|
const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
|
|
11421
|
-
const before =
|
|
11800
|
+
const before = blockSyncData();
|
|
11422
11801
|
|
|
11423
11802
|
syncApplying = true;
|
|
11424
11803
|
|
|
@@ -11465,13 +11844,13 @@
|
|
|
11465
11844
|
syncApplying = false;
|
|
11466
11845
|
}
|
|
11467
11846
|
|
|
11468
|
-
const changed = before !==
|
|
11847
|
+
const changed = before !== blockSyncData();
|
|
11469
11848
|
|
|
11470
11849
|
if (changed && initialized) {
|
|
11471
11850
|
refreshAfterBlockChange();
|
|
11472
11851
|
}
|
|
11473
11852
|
|
|
11474
|
-
return changed;
|
|
11853
|
+
return changed || recentChanged || searchChanged;
|
|
11475
11854
|
};
|
|
11476
11855
|
|
|
11477
11856
|
const saveSyncUndo = (before, incoming, summary, from) => {
|
|
@@ -11501,6 +11880,20 @@
|
|
|
11501
11880
|
return /^https:\/\/[^/\s]+/.test(base) ? `${base}${path}` : '';
|
|
11502
11881
|
};
|
|
11503
11882
|
|
|
11883
|
+
const syncError = (message, { transient = false, quiet = false } = {}) => Object.assign(new Error(message), { transient, quiet });
|
|
11884
|
+
const statusError = status => syncError(status === 413 ? '내용이 너무 큽니다' : `서버 응답 ${status}`, { transient: status >= 500 || status === 429 || status === 408 });
|
|
11885
|
+
|
|
11886
|
+
const syncControllers = new Set();
|
|
11887
|
+
|
|
11888
|
+
const abortSyncRequests = () => {
|
|
11889
|
+
syncControllers.forEach(controller => {
|
|
11890
|
+
if (!controller.dcKeep) {
|
|
11891
|
+
controller.dcQuiet = true;
|
|
11892
|
+
controller.abort();
|
|
11893
|
+
}
|
|
11894
|
+
});
|
|
11895
|
+
};
|
|
11896
|
+
|
|
11504
11897
|
const syncRequest = async (path, { method = 'GET', headers = {}, body, keepalive = false } = {}) => {
|
|
11505
11898
|
const url = syncEndpoint(path);
|
|
11506
11899
|
|
|
@@ -11512,22 +11905,42 @@
|
|
|
11512
11905
|
const timer = controller ? setTimeout(() => controller.abort(), 15000) : null;
|
|
11513
11906
|
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
11514
11907
|
|
|
11908
|
+
if (controller) {
|
|
11909
|
+
controller.dcKeep = keepalive;
|
|
11910
|
+
syncControllers.add(controller);
|
|
11911
|
+
}
|
|
11912
|
+
|
|
11515
11913
|
try {
|
|
11516
|
-
|
|
11914
|
+
let response;
|
|
11915
|
+
|
|
11916
|
+
try {
|
|
11917
|
+
response = await fetch(url, {
|
|
11517
11918
|
method,
|
|
11518
11919
|
headers: { 'Content-Type': 'application/json', ...headers },
|
|
11519
11920
|
body: payload,
|
|
11520
11921
|
credentials: 'omit',
|
|
11521
11922
|
cache: 'no-store',
|
|
11522
11923
|
referrerPolicy: 'no-referrer',
|
|
11523
|
-
|
|
11524
|
-
|
|
11525
|
-
|
|
11924
|
+
keepalive: keepalive && (!payload || payload.length < 60000),
|
|
11925
|
+
signal: controller ? controller.signal : undefined
|
|
11926
|
+
});
|
|
11927
|
+
} catch (error) {
|
|
11928
|
+
if (controller && controller.dcQuiet) {
|
|
11929
|
+
throw syncError('페이지를 떠나며 멈춤', { quiet: true });
|
|
11930
|
+
}
|
|
11931
|
+
|
|
11932
|
+
throw syncError(error && error.name === 'AbortError' ? '서버 응답이 없습니다' : '서버에 연결하지 못했습니다', { transient: true });
|
|
11933
|
+
}
|
|
11934
|
+
|
|
11526
11935
|
const data = await response.json().catch(() => ({}));
|
|
11527
11936
|
|
|
11528
11937
|
return { status: response.status, data };
|
|
11529
11938
|
} finally {
|
|
11530
11939
|
clearTimeout(timer);
|
|
11940
|
+
|
|
11941
|
+
if (controller) {
|
|
11942
|
+
syncControllers.delete(controller);
|
|
11943
|
+
}
|
|
11531
11944
|
}
|
|
11532
11945
|
};
|
|
11533
11946
|
|
|
@@ -11610,12 +12023,35 @@
|
|
|
11610
12023
|
};
|
|
11611
12024
|
|
|
11612
12025
|
let syncRunning = false;
|
|
12026
|
+
let syncRunId = 0;
|
|
12027
|
+
let syncRunSince = 0;
|
|
12028
|
+
let syncEpoch = 0;
|
|
12029
|
+
let pageLeaving = false;
|
|
11613
12030
|
let syncAgain = false;
|
|
11614
12031
|
let syncTimer = null;
|
|
11615
12032
|
let syncPending = false;
|
|
11616
|
-
let lastSyncAt = 0;
|
|
11617
12033
|
let lastErrorMsg = '';
|
|
11618
12034
|
|
|
12035
|
+
const readSyncRt = () => readObject(STORAGE.syncRt);
|
|
12036
|
+
const writeSyncRt = patch => writeJson(STORAGE.syncRt, { ...readSyncRt(), ...patch });
|
|
12037
|
+
const lastSyncTry = () => Number(readSyncRt().lastTry) || 0;
|
|
12038
|
+
|
|
12039
|
+
const resetStuckSync = (force = false) => {
|
|
12040
|
+
if (syncRunning && (force || Date.now() - syncRunSince > SYNC_STUCK_MS)) {
|
|
12041
|
+
syncRunId++;
|
|
12042
|
+
syncRunning = false;
|
|
12043
|
+
releaseSyncLock();
|
|
12044
|
+
setSyncPhase('');
|
|
12045
|
+
}
|
|
12046
|
+
};
|
|
12047
|
+
|
|
12048
|
+
const syncDueOnReturn = () => {
|
|
12049
|
+
const rt = readSyncRt();
|
|
12050
|
+
const state = readObject(STORAGE.syncState);
|
|
12051
|
+
|
|
12052
|
+
return syncPending || rt.dirty === true || (state.at && state.ok === false) || Date.now() - lastSyncTry() > SYNC_MIN_GAP;
|
|
12053
|
+
};
|
|
12054
|
+
|
|
11619
12055
|
const syncReady = () => {
|
|
11620
12056
|
const conf = readSyncConf();
|
|
11621
12057
|
|
|
@@ -11649,6 +12085,8 @@
|
|
|
11649
12085
|
return false;
|
|
11650
12086
|
}
|
|
11651
12087
|
|
|
12088
|
+
resetStuckSync();
|
|
12089
|
+
|
|
11652
12090
|
if (syncRunning) {
|
|
11653
12091
|
syncAgain = true;
|
|
11654
12092
|
return false;
|
|
@@ -11665,7 +12103,7 @@
|
|
|
11665
12103
|
return false;
|
|
11666
12104
|
}
|
|
11667
12105
|
|
|
11668
|
-
if (!takeSyncLock(manual)) {
|
|
12106
|
+
if (!takeSyncLock(manual || leaving)) {
|
|
11669
12107
|
scheduleSync(SYNC_LOCK_MS / 2);
|
|
11670
12108
|
return false;
|
|
11671
12109
|
}
|
|
@@ -11673,7 +12111,11 @@
|
|
|
11673
12111
|
syncRunning = true;
|
|
11674
12112
|
syncPending = false;
|
|
11675
12113
|
clearTimeout(syncTimer);
|
|
11676
|
-
|
|
12114
|
+
syncRunSince = Date.now();
|
|
12115
|
+
writeSyncRt({ lastTry: syncRunSince });
|
|
12116
|
+
|
|
12117
|
+
const runId = ++syncRunId;
|
|
12118
|
+
const epoch = syncEpoch;
|
|
11677
12119
|
|
|
11678
12120
|
const key = readSyncKey();
|
|
11679
12121
|
const conf = readSyncConf();
|
|
@@ -11696,7 +12138,7 @@
|
|
|
11696
12138
|
}
|
|
11697
12139
|
|
|
11698
12140
|
if (got.status !== 200) {
|
|
11699
|
-
throw
|
|
12141
|
+
throw statusError(got.status);
|
|
11700
12142
|
}
|
|
11701
12143
|
|
|
11702
12144
|
setSyncPhase('merge');
|
|
@@ -11736,7 +12178,7 @@
|
|
|
11736
12178
|
}
|
|
11737
12179
|
|
|
11738
12180
|
if (put.status !== 200) {
|
|
11739
|
-
throw
|
|
12181
|
+
throw statusError(put.status);
|
|
11740
12182
|
}
|
|
11741
12183
|
|
|
11742
12184
|
newRev = Number(put.data.rev) || rev + 1;
|
|
@@ -11772,6 +12214,7 @@
|
|
|
11772
12214
|
const msg = changed ? `${from}에서 받음 · ${summary}` : sentSummary ? `올림 · ${sentSummary}` : '최신 상태입니다';
|
|
11773
12215
|
|
|
11774
12216
|
writeSyncState({ ok: true, msg });
|
|
12217
|
+
writeSyncRt({ fails: 0, dirty: syncPending });
|
|
11775
12218
|
lastErrorMsg = '';
|
|
11776
12219
|
|
|
11777
12220
|
if (changed) {
|
|
@@ -11796,31 +12239,60 @@
|
|
|
11796
12239
|
return true;
|
|
11797
12240
|
}
|
|
11798
12241
|
|
|
11799
|
-
throw
|
|
12242
|
+
throw syncError('다른 기기와 동시에 바뀌어 잠시 뒤 다시 맞춥니다', { transient: true });
|
|
11800
12243
|
} catch (error) {
|
|
11801
|
-
|
|
11802
|
-
|
|
11803
|
-
writeSyncState({ ok: false, msg });
|
|
12244
|
+
syncPending = true;
|
|
11804
12245
|
|
|
11805
|
-
if (
|
|
11806
|
-
|
|
12246
|
+
if (runId !== syncRunId || (error && error.quiet) || pageLeaving || leaving || epoch !== syncEpoch || document.visibilityState === 'hidden') {
|
|
12247
|
+
writeSyncRt({ dirty: true });
|
|
11807
12248
|
|
|
11808
|
-
if (
|
|
11809
|
-
|
|
12249
|
+
if (runId === syncRunId && !pageLeaving && document.visibilityState !== 'hidden') {
|
|
12250
|
+
scheduleSync(SYNC_RETRY[0]);
|
|
11810
12251
|
}
|
|
12252
|
+
|
|
12253
|
+
return false;
|
|
11811
12254
|
}
|
|
11812
12255
|
|
|
11813
|
-
|
|
11814
|
-
|
|
11815
|
-
|
|
12256
|
+
if (navigator.onLine === false) {
|
|
12257
|
+
writeSyncRt({ dirty: true });
|
|
12258
|
+
writeSyncState({ ok: false, offline: true, msg: '오프라인 · 연결되면 맞춥니다' });
|
|
12259
|
+
return false;
|
|
12260
|
+
}
|
|
11816
12261
|
|
|
11817
|
-
|
|
12262
|
+
const transient = !!(error && (error.transient || error.name === 'TypeError'));
|
|
12263
|
+
const fails = (Number(readSyncRt().fails) || 0) + 1;
|
|
12264
|
+
|
|
12265
|
+
writeSyncRt({ fails, dirty: true });
|
|
12266
|
+
|
|
12267
|
+
if (transient && !manual && fails < SYNC_FAIL_SHOW) {
|
|
12268
|
+
scheduleSync(SYNC_RETRY[fails - 1]);
|
|
12269
|
+
return false;
|
|
12270
|
+
}
|
|
12271
|
+
|
|
12272
|
+
const msg = String((error && error.message) || error);
|
|
12273
|
+
|
|
12274
|
+
writeSyncState({ ok: false, msg });
|
|
12275
|
+
|
|
12276
|
+
if (manual || msg !== lastErrorMsg) {
|
|
12277
|
+
addSyncLog('error', msg);
|
|
12278
|
+
|
|
12279
|
+
if (manual || lastErrorMsg === '') {
|
|
12280
|
+
notifySync('error', `⚠️ 동기화 실패 · ${msg}`, { actionLabel: '다시 시도', onAction: () => syncNow({ manual: true }), duration: 5000, force: manual });
|
|
12281
|
+
}
|
|
12282
|
+
}
|
|
12283
|
+
|
|
12284
|
+
lastErrorMsg = msg;
|
|
12285
|
+
scheduleSync(transient ? SYNC_RETRY[Math.min(fails - 1, SYNC_RETRY.length - 1)] : SYNC_POLL);
|
|
12286
|
+
|
|
12287
|
+
return false;
|
|
11818
12288
|
} finally {
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
12289
|
+
if (runId === syncRunId) {
|
|
12290
|
+
syncRunning = false;
|
|
12291
|
+
releaseSyncLock();
|
|
12292
|
+
setSyncPhase('');
|
|
12293
|
+
}
|
|
11822
12294
|
|
|
11823
|
-
if (syncAgain) {
|
|
12295
|
+
if (syncAgain && runId === syncRunId) {
|
|
11824
12296
|
syncAgain = false;
|
|
11825
12297
|
scheduleSync(SYNC_DEBOUNCE);
|
|
11826
12298
|
}
|
|
@@ -11870,6 +12342,7 @@
|
|
|
11870
12342
|
storageWriteListeners.push(key => {
|
|
11871
12343
|
if (!syncApplying && SYNCED_STORAGE.includes(key) && syncReady()) {
|
|
11872
12344
|
syncPending = true;
|
|
12345
|
+
writeSyncRt({ dirty: true });
|
|
11873
12346
|
scheduleSync(SYNC_DEBOUNCE);
|
|
11874
12347
|
}
|
|
11875
12348
|
});
|
|
@@ -11904,7 +12377,7 @@
|
|
|
11904
12377
|
return;
|
|
11905
12378
|
}
|
|
11906
12379
|
|
|
11907
|
-
if (document.visibilityState === 'visible' && Date.now() -
|
|
12380
|
+
if (document.visibilityState === 'visible' && Date.now() - lastSyncTry() > SYNC_POLL - 1000) {
|
|
11908
12381
|
syncNow();
|
|
11909
12382
|
}
|
|
11910
12383
|
}, SYNC_POLL);
|
|
@@ -11961,29 +12434,48 @@
|
|
|
11961
12434
|
syncStarted = true;
|
|
11962
12435
|
noteScriptVersion();
|
|
11963
12436
|
|
|
11964
|
-
|
|
12437
|
+
const onReturn = restored => {
|
|
12438
|
+
pageLeaving = false;
|
|
12439
|
+
|
|
11965
12440
|
if (!syncReady()) {
|
|
11966
12441
|
return;
|
|
11967
12442
|
}
|
|
11968
12443
|
|
|
11969
|
-
|
|
11970
|
-
|
|
11971
|
-
|
|
11972
|
-
|
|
11973
|
-
}
|
|
11974
|
-
|
|
12444
|
+
resetStuckSync(restored);
|
|
12445
|
+
|
|
12446
|
+
if (syncDueOnReturn() && !syncRunning) {
|
|
12447
|
+
scheduleSync(800);
|
|
12448
|
+
}
|
|
12449
|
+
};
|
|
12450
|
+
|
|
12451
|
+
const onLeave = unloading => {
|
|
12452
|
+
syncEpoch++;
|
|
12453
|
+
pageLeaving = unloading;
|
|
12454
|
+
|
|
12455
|
+
if (unloading) {
|
|
12456
|
+
abortSyncRequests();
|
|
11975
12457
|
}
|
|
11976
|
-
});
|
|
11977
12458
|
|
|
11978
|
-
|
|
12459
|
+
clearTimeout(syncTimer);
|
|
12460
|
+
|
|
11979
12461
|
if (syncReady() && syncPending && !syncRunning) {
|
|
11980
12462
|
syncNow({ leaving: true });
|
|
11981
12463
|
}
|
|
12464
|
+
};
|
|
12465
|
+
|
|
12466
|
+
document.addEventListener('visibilitychange', () => {
|
|
12467
|
+
if (document.visibilityState === 'visible') {
|
|
12468
|
+
onReturn(false);
|
|
12469
|
+
} else {
|
|
12470
|
+
onLeave(false);
|
|
12471
|
+
}
|
|
11982
12472
|
});
|
|
11983
12473
|
|
|
12474
|
+
window.addEventListener('pagehide', () => onLeave(true));
|
|
12475
|
+
|
|
11984
12476
|
window.addEventListener('pageshow', event => {
|
|
11985
|
-
if (event.persisted
|
|
11986
|
-
|
|
12477
|
+
if (event.persisted) {
|
|
12478
|
+
onReturn(true);
|
|
11987
12479
|
}
|
|
11988
12480
|
});
|
|
11989
12481
|
|
|
@@ -12010,7 +12502,10 @@
|
|
|
12010
12502
|
|
|
12011
12503
|
if (syncReady()) {
|
|
12012
12504
|
startSyncPoll();
|
|
12013
|
-
|
|
12505
|
+
|
|
12506
|
+
if (syncDueOnReturn()) {
|
|
12507
|
+
scheduleSync(1500);
|
|
12508
|
+
}
|
|
12014
12509
|
}
|
|
12015
12510
|
};
|
|
12016
12511
|
|
|
@@ -12575,6 +13070,1095 @@
|
|
|
12575
13070
|
return box;
|
|
12576
13071
|
};
|
|
12577
13072
|
|
|
13073
|
+
// =========================================================
|
|
13074
|
+
// 13-3. 최근 방문 갤러리 · 사이트 알림
|
|
13075
|
+
// =========================================================
|
|
13076
|
+
|
|
13077
|
+
const RECENT_LIMIT = 50;
|
|
13078
|
+
const RECENT_NATIVE_CAP = IS_MOBILE_SITE ? 24 : 50;
|
|
13079
|
+
const RECENT_KINDS = ['G', 'M', 'N', 'P'];
|
|
13080
|
+
const RECENT_ID_RE = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
13081
|
+
|
|
13082
|
+
const recentKeyOf = item => `${item.k}:${item.id}`;
|
|
13083
|
+
|
|
13084
|
+
const normalizeRecent = item => {
|
|
13085
|
+
if (!item || typeof item !== 'object' || typeof item.id !== 'string' || !RECENT_ID_RE.test(item.id)) {
|
|
13086
|
+
return null;
|
|
13087
|
+
}
|
|
13088
|
+
|
|
13089
|
+
return {
|
|
13090
|
+
k: RECENT_KINDS.includes(item.k) ? item.k : 'G',
|
|
13091
|
+
id: item.id,
|
|
13092
|
+
n: cleanText(item.n).slice(0, 40),
|
|
13093
|
+
at: Number(item.at) || 0
|
|
13094
|
+
};
|
|
13095
|
+
};
|
|
13096
|
+
|
|
13097
|
+
const tidyRecent = list => {
|
|
13098
|
+
const best = new Map();
|
|
13099
|
+
|
|
13100
|
+
list.map(normalizeRecent).filter(Boolean).forEach(item => {
|
|
13101
|
+
const key = recentKeyOf(item);
|
|
13102
|
+
const prev = best.get(key);
|
|
13103
|
+
|
|
13104
|
+
if (!prev || item.at > prev.at || (item.at === prev.at && !prev.n && item.n)) {
|
|
13105
|
+
best.set(key, prev && !item.n ? { ...item, n: prev.n } : item);
|
|
13106
|
+
}
|
|
13107
|
+
});
|
|
13108
|
+
|
|
13109
|
+
return [...best.values()].sort((a, b) => b.at - a.at).slice(0, RECENT_LIMIT);
|
|
13110
|
+
};
|
|
13111
|
+
|
|
13112
|
+
let recentGalls = tidyRecent(readArray(STORAGE.recent));
|
|
13113
|
+
|
|
13114
|
+
const saveRecent = () => {
|
|
13115
|
+
recentGalls = tidyRecent(recentGalls);
|
|
13116
|
+
writeJson(STORAGE.recent, recentGalls);
|
|
13117
|
+
};
|
|
13118
|
+
|
|
13119
|
+
const recentUrl = item => {
|
|
13120
|
+
if (IS_MOBILE_SITE) {
|
|
13121
|
+
const base = item.k === 'N' ? 'mini' : item.k === 'P' ? 'person' : 'board';
|
|
13122
|
+
|
|
13123
|
+
return `https://m.dcinside.com/${base}/${encodeURIComponent(item.id)}`;
|
|
13124
|
+
}
|
|
13125
|
+
|
|
13126
|
+
const prefix = { G: '', M: '/mgallery', N: '/mini', P: '/person' }[item.k] || '';
|
|
13127
|
+
|
|
13128
|
+
return `https://gall.dcinside.com${prefix}/board/lists/?id=${encodeURIComponent(item.id)}`;
|
|
13129
|
+
};
|
|
13130
|
+
|
|
13131
|
+
const RECENT_KIND_LABEL = { G: '', M: '마이너', N: '미니', P: '인물' };
|
|
13132
|
+
|
|
13133
|
+
const MOBILE_KIND_MARK = { M: '$minor$', N: '$mini$', P: '$person$' };
|
|
13134
|
+
const PC_KIND = { G: 'G', M: 'M', N: 'MI', P: 'PR' };
|
|
13135
|
+
const PC_KIND_BACK = { G: 'G', M: 'M', MI: 'N', PR: 'P' };
|
|
13136
|
+
|
|
13137
|
+
const readNativeRecent = () => {
|
|
13138
|
+
try {
|
|
13139
|
+
if (IS_MOBILE_SITE) {
|
|
13140
|
+
const raw = unescape(localStorage.getItem('m_dcinside_lately') || '');
|
|
13141
|
+
|
|
13142
|
+
return raw.split(',').map(part => {
|
|
13143
|
+
const cut = part.indexOf('|');
|
|
13144
|
+
|
|
13145
|
+
if (cut < 1) {
|
|
13146
|
+
return null;
|
|
13147
|
+
}
|
|
13148
|
+
|
|
13149
|
+
let name = part.slice(cut + 1);
|
|
13150
|
+
let k = 'G';
|
|
13151
|
+
|
|
13152
|
+
Object.entries(MOBILE_KIND_MARK).forEach(([kind, mark]) => {
|
|
13153
|
+
if (name.includes(mark)) {
|
|
13154
|
+
k = kind;
|
|
13155
|
+
name = name.replace(mark, '');
|
|
13156
|
+
}
|
|
13157
|
+
});
|
|
13158
|
+
|
|
13159
|
+
return normalizeRecent({ k, id: part.slice(0, cut).trim(), n: name, at: 0 });
|
|
13160
|
+
}).filter(Boolean);
|
|
13161
|
+
}
|
|
13162
|
+
|
|
13163
|
+
if (!/^gall\.dcinside\.com$/i.test(location.hostname)) {
|
|
13164
|
+
return null;
|
|
13165
|
+
}
|
|
13166
|
+
|
|
13167
|
+
const list = JSON.parse(localStorage.getItem('lately_gallery') || 'null');
|
|
13168
|
+
|
|
13169
|
+
return Array.isArray(list)
|
|
13170
|
+
? list.map(item => item && normalizeRecent({ k: PC_KIND_BACK[item.type] || 'G', id: String(item.id || ''), n: item.name, at: 0 })).filter(Boolean)
|
|
13171
|
+
: [];
|
|
13172
|
+
} catch {
|
|
13173
|
+
return null;
|
|
13174
|
+
}
|
|
13175
|
+
};
|
|
13176
|
+
|
|
13177
|
+
const writeNativeRecent = list => {
|
|
13178
|
+
const top = list.slice(0, RECENT_NATIVE_CAP);
|
|
13179
|
+
|
|
13180
|
+
try {
|
|
13181
|
+
if (IS_MOBILE_SITE) {
|
|
13182
|
+
const text = top.map(item => `${item.id}|${(item.n || item.id).replace(/[,|]/g, ' ')}${MOBILE_KIND_MARK[item.k] ? ` ${MOBILE_KIND_MARK[item.k]}` : ''},`).join('');
|
|
13183
|
+
|
|
13184
|
+
localStorage.setItem('m_dcinside_lately', escape(text));
|
|
13185
|
+
} else if (/^gall\.dcinside\.com$/i.test(location.hostname)) {
|
|
13186
|
+
const dir = { G: '', M: 'mgallery/', N: 'mini/', P: 'person/' };
|
|
13187
|
+
|
|
13188
|
+
localStorage.setItem('lately_gallery', JSON.stringify(top.map(item => ({
|
|
13189
|
+
id: item.id,
|
|
13190
|
+
name: item.n || item.id,
|
|
13191
|
+
type: PC_KIND[item.k] || 'G',
|
|
13192
|
+
link: `gall.dcinside.com/${dir[item.k] || ''}board/lists?id=${item.id}`
|
|
13193
|
+
}))));
|
|
13194
|
+
} else {
|
|
13195
|
+
return;
|
|
13196
|
+
}
|
|
13197
|
+
|
|
13198
|
+
writeJson(STORAGE.recentNative, top.map(recentKeyOf));
|
|
13199
|
+
} catch {
|
|
13200
|
+
}
|
|
13201
|
+
};
|
|
13202
|
+
|
|
13203
|
+
const importNativeRecent = () => {
|
|
13204
|
+
const native = readNativeRecent();
|
|
13205
|
+
|
|
13206
|
+
if (!native) {
|
|
13207
|
+
return false;
|
|
13208
|
+
}
|
|
13209
|
+
|
|
13210
|
+
const before = JSON.stringify(recentGalls);
|
|
13211
|
+
const nativeKeys = new Set(native.map(recentKeyOf));
|
|
13212
|
+
const snapshot = readArray(STORAGE.recentNative).filter(key => typeof key === 'string');
|
|
13213
|
+
|
|
13214
|
+
if (snapshot.length) {
|
|
13215
|
+
const snapSet = new Set(snapshot);
|
|
13216
|
+
const added = native.filter(item => !snapSet.has(recentKeyOf(item))).length;
|
|
13217
|
+
const overflow = Math.max(0, snapshot.length + added - RECENT_NATIVE_CAP);
|
|
13218
|
+
const kept = snapshot.slice(0, snapshot.length - overflow);
|
|
13219
|
+
const removed = new Set(kept.filter(key => !nativeKeys.has(key)));
|
|
13220
|
+
|
|
13221
|
+
if (removed.size) {
|
|
13222
|
+
recentGalls = recentGalls.filter(item => !removed.has(recentKeyOf(item)));
|
|
13223
|
+
}
|
|
13224
|
+
}
|
|
13225
|
+
|
|
13226
|
+
const mine = new Map(recentGalls.map(item => [recentKeyOf(item), item]));
|
|
13227
|
+
let prevAt = Date.now();
|
|
13228
|
+
|
|
13229
|
+
native.forEach(item => {
|
|
13230
|
+
const known = mine.get(recentKeyOf(item));
|
|
13231
|
+
|
|
13232
|
+
if (known) {
|
|
13233
|
+
prevAt = Math.min(prevAt, known.at || prevAt);
|
|
13234
|
+
|
|
13235
|
+
if (!known.n && item.n) {
|
|
13236
|
+
known.n = item.n;
|
|
13237
|
+
}
|
|
13238
|
+
} else {
|
|
13239
|
+
prevAt -= 1000;
|
|
13240
|
+
recentGalls.push({ ...item, at: prevAt });
|
|
13241
|
+
}
|
|
13242
|
+
});
|
|
13243
|
+
|
|
13244
|
+
recentGalls = tidyRecent(recentGalls);
|
|
13245
|
+
|
|
13246
|
+
const changed = JSON.stringify(recentGalls) !== before;
|
|
13247
|
+
|
|
13248
|
+
if (changed) {
|
|
13249
|
+
writeJson(STORAGE.recent, recentGalls);
|
|
13250
|
+
}
|
|
13251
|
+
|
|
13252
|
+
return changed;
|
|
13253
|
+
};
|
|
13254
|
+
|
|
13255
|
+
const exportNativeRecent = () => {
|
|
13256
|
+
const native = readNativeRecent();
|
|
13257
|
+
|
|
13258
|
+
if (!native) {
|
|
13259
|
+
return;
|
|
13260
|
+
}
|
|
13261
|
+
|
|
13262
|
+
const want = recentGalls.slice(0, RECENT_NATIVE_CAP);
|
|
13263
|
+
|
|
13264
|
+
if (JSON.stringify(native.map(recentKeyOf)) !== JSON.stringify(want.map(recentKeyOf))) {
|
|
13265
|
+
writeNativeRecent(want);
|
|
13266
|
+
} else {
|
|
13267
|
+
writeJson(STORAGE.recentNative, want.map(recentKeyOf));
|
|
13268
|
+
}
|
|
13269
|
+
};
|
|
13270
|
+
|
|
13271
|
+
const currentGalleryVisit = () => {
|
|
13272
|
+
const id = getGalleryId();
|
|
13273
|
+
|
|
13274
|
+
if (!id || id === HOME_GALLERY_KEY || !RECENT_ID_RE.test(id)) {
|
|
13275
|
+
return null;
|
|
13276
|
+
}
|
|
13277
|
+
|
|
13278
|
+
let k = 'G';
|
|
13279
|
+
let name = '';
|
|
13280
|
+
|
|
13281
|
+
if (IS_MOBILE_SITE) {
|
|
13282
|
+
if (!/^\/(board|mini|person)\//.test(location.pathname)) {
|
|
13283
|
+
return null;
|
|
13284
|
+
}
|
|
13285
|
+
|
|
13286
|
+
k = /^\/mini\//.test(location.pathname)
|
|
13287
|
+
? 'N'
|
|
13288
|
+
: /^\/person\//.test(location.pathname)
|
|
13289
|
+
? 'P'
|
|
13290
|
+
: (document.getElementById('is_minor')?.value || '') !== '' ? 'M' : 'G';
|
|
13291
|
+
name = document.querySelector('.gall-tit-box .gall-tit')?.textContent || '';
|
|
13292
|
+
} else {
|
|
13293
|
+
if (!/^\/(mgallery\/|mini\/|person\/)?board\/(lists|view)/.test(location.pathname)) {
|
|
13294
|
+
return null;
|
|
13295
|
+
}
|
|
13296
|
+
|
|
13297
|
+
k = /^\/mgallery\//.test(location.pathname) ? 'M' : /^\/mini\//.test(location.pathname) ? 'N' : /^\/person\//.test(location.pathname) ? 'P' : 'G';
|
|
13298
|
+
|
|
13299
|
+
const head = document.querySelector('.page_head h2 a') || document.querySelector('.page_head h2');
|
|
13300
|
+
|
|
13301
|
+
name = head ? [...head.childNodes].filter(node => node.nodeType === 3).map(node => node.textContent).join(' ') : '';
|
|
13302
|
+
|
|
13303
|
+
if (!cleanText(name)) {
|
|
13304
|
+
const og = document.querySelector('meta[property="og:title"]')?.content || '';
|
|
13305
|
+
|
|
13306
|
+
name = og.split(' - ').slice(-2, -1)[0] || og.split(' - ')[0] || '';
|
|
13307
|
+
}
|
|
13308
|
+
}
|
|
13309
|
+
|
|
13310
|
+
name = cleanText(name).replace(/\s*(마이너|미니|인물)?\s*갤러리$/, '');
|
|
13311
|
+
|
|
13312
|
+
return normalizeRecent({ k, id, n: name || id, at: Date.now() });
|
|
13313
|
+
};
|
|
13314
|
+
|
|
13315
|
+
const recordGalleryVisit = () => {
|
|
13316
|
+
const visit = currentGalleryVisit();
|
|
13317
|
+
|
|
13318
|
+
if (!visit) {
|
|
13319
|
+
return;
|
|
13320
|
+
}
|
|
13321
|
+
|
|
13322
|
+
const prev = recentGalls.find(item => recentKeyOf(item) === recentKeyOf(visit));
|
|
13323
|
+
|
|
13324
|
+
if (prev && recentGalls[0] === prev && Date.now() - prev.at < 60000 && prev.n === visit.n) {
|
|
13325
|
+
return;
|
|
13326
|
+
}
|
|
13327
|
+
|
|
13328
|
+
recentGalls = [visit, ...recentGalls.filter(item => item !== prev)];
|
|
13329
|
+
saveRecent();
|
|
13330
|
+
};
|
|
13331
|
+
|
|
13332
|
+
const initRecentEarly = () => {
|
|
13333
|
+
importNativeRecent();
|
|
13334
|
+
exportNativeRecent();
|
|
13335
|
+
};
|
|
13336
|
+
|
|
13337
|
+
const initRecent = () => {
|
|
13338
|
+
importNativeRecent();
|
|
13339
|
+
recordGalleryVisit();
|
|
13340
|
+
exportNativeRecent();
|
|
13341
|
+
|
|
13342
|
+
const follow = () => {
|
|
13343
|
+
if (document.visibilityState === 'visible' && importNativeRecent()) {
|
|
13344
|
+
exportNativeRecent();
|
|
13345
|
+
}
|
|
13346
|
+
};
|
|
13347
|
+
|
|
13348
|
+
setTimeout(follow, 3000);
|
|
13349
|
+
document.addEventListener('visibilitychange', follow);
|
|
13350
|
+
};
|
|
13351
|
+
|
|
13352
|
+
const collectRecentForSync = () => recentGalls.map(item => ({ ...item }));
|
|
13353
|
+
|
|
13354
|
+
const mergeRecent = (local = [], base, remote) => {
|
|
13355
|
+
if (!Array.isArray(remote)) {
|
|
13356
|
+
return tidyRecent(local || []);
|
|
13357
|
+
}
|
|
13358
|
+
|
|
13359
|
+
const pick = new Map();
|
|
13360
|
+
|
|
13361
|
+
[...(remote || []), ...(local || [])].map(normalizeRecent).filter(Boolean).forEach(item => {
|
|
13362
|
+
const key = recentKeyOf(item);
|
|
13363
|
+
const prev = pick.get(key);
|
|
13364
|
+
|
|
13365
|
+
if (!prev || item.at > prev.at) {
|
|
13366
|
+
pick.set(key, prev && !item.n ? { ...item, n: prev.n } : item);
|
|
13367
|
+
}
|
|
13368
|
+
});
|
|
13369
|
+
|
|
13370
|
+
const keys = mergeList(
|
|
13371
|
+
(local || []).map(item => recentKeyOf(item)),
|
|
13372
|
+
Array.isArray(base) ? base.map(item => recentKeyOf(item)) : null,
|
|
13373
|
+
remote.map(item => recentKeyOf(item))
|
|
13374
|
+
);
|
|
13375
|
+
|
|
13376
|
+
return tidyRecent(keys.map(key => pick.get(key)).filter(Boolean));
|
|
13377
|
+
};
|
|
13378
|
+
|
|
13379
|
+
const applyRecentFromSync = list => {
|
|
13380
|
+
if (!Array.isArray(list)) {
|
|
13381
|
+
return false;
|
|
13382
|
+
}
|
|
13383
|
+
|
|
13384
|
+
const next = tidyRecent(list);
|
|
13385
|
+
|
|
13386
|
+
if (JSON.stringify(next) === JSON.stringify(recentGalls)) {
|
|
13387
|
+
return false;
|
|
13388
|
+
}
|
|
13389
|
+
|
|
13390
|
+
recentGalls = next;
|
|
13391
|
+
writeJson(STORAGE.recent, recentGalls);
|
|
13392
|
+
exportNativeRecent();
|
|
13393
|
+
|
|
13394
|
+
return true;
|
|
13395
|
+
};
|
|
13396
|
+
|
|
13397
|
+
// =========================================================
|
|
13398
|
+
|
|
13399
|
+
// =========================================================
|
|
13400
|
+
|
|
13401
|
+
const ALARM_TAGS = { R: '댓글', RR: '답글', P: '펌', hit: '힛갤', B: '실베', EC: '동영상', G: '방명록' };
|
|
13402
|
+
const ALARM_POLL_GAP = 3 * 60 * 1000;
|
|
13403
|
+
const ALARM_BUBBLE_MAX = 3;
|
|
13404
|
+
const ALARM_BUBBLE_FRESH = 10 * 60 * 1000;
|
|
13405
|
+
|
|
13406
|
+
const siteUserId = () => cleanText(document.getElementById('user_id')?.value || '');
|
|
13407
|
+
|
|
13408
|
+
const alarmStorageKeys = () => {
|
|
13409
|
+
try {
|
|
13410
|
+
if (IS_MOBILE_SITE) {
|
|
13411
|
+
const id = siteUserId();
|
|
13412
|
+
|
|
13413
|
+
return id ? [`alarm_notification_${id}`] : [];
|
|
13414
|
+
}
|
|
13415
|
+
|
|
13416
|
+
if (!/^gall\.dcinside\.com$/i.test(location.hostname)) {
|
|
13417
|
+
return [];
|
|
13418
|
+
}
|
|
13419
|
+
|
|
13420
|
+
const keys = [];
|
|
13421
|
+
|
|
13422
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
13423
|
+
const key = localStorage.key(i);
|
|
13424
|
+
|
|
13425
|
+
if (/^alarm_list/.test(key || '')) {
|
|
13426
|
+
keys.push(key);
|
|
13427
|
+
}
|
|
13428
|
+
}
|
|
13429
|
+
|
|
13430
|
+
return keys;
|
|
13431
|
+
} catch {
|
|
13432
|
+
return [];
|
|
13433
|
+
}
|
|
13434
|
+
};
|
|
13435
|
+
|
|
13436
|
+
const readAlarmRaw = () => alarmStorageKeys().map(key => {
|
|
13437
|
+
try {
|
|
13438
|
+
return localStorage.getItem(key) || '';
|
|
13439
|
+
} catch {
|
|
13440
|
+
return '';
|
|
13441
|
+
}
|
|
13442
|
+
}).join('\u0001');
|
|
13443
|
+
|
|
13444
|
+
const mobileAlarmHref = item => {
|
|
13445
|
+
const raw = String(item.id || '');
|
|
13446
|
+
const no = String(item.no || '').replace(/\D/g, '');
|
|
13447
|
+
|
|
13448
|
+
if (item.type === 'B' || raw === 'dcbest') {
|
|
13449
|
+
return no ? `https://m.dcinside.com/board/dcbest/${no}` : '';
|
|
13450
|
+
}
|
|
13451
|
+
|
|
13452
|
+
if (item.type === 'G') {
|
|
13453
|
+
const me = siteUserId();
|
|
13454
|
+
|
|
13455
|
+
return me ? `https://m.dcinside.com/gallog/${encodeURIComponent(me)}/guestbook` : '';
|
|
13456
|
+
}
|
|
13457
|
+
|
|
13458
|
+
const base = raw.startsWith('mi$') ? 'mini' : raw.startsWith('pr$') ? 'person' : 'board';
|
|
13459
|
+
const id = raw.replace(/^(mi|pr)\$/, '');
|
|
13460
|
+
|
|
13461
|
+
if (!id || !no || id === 'hit') {
|
|
13462
|
+
return id === 'hit' && no ? `https://m.dcinside.com/board/hit/${no}` : '';
|
|
13463
|
+
}
|
|
13464
|
+
|
|
13465
|
+
const comment = String(item.comment_no || '').replace(/\D/g, '');
|
|
13466
|
+
|
|
13467
|
+
return `https://m.dcinside.com/${base}/${encodeURIComponent(id)}/${no}${comment ? `?comment=${comment}` : ''}`;
|
|
13468
|
+
};
|
|
13469
|
+
|
|
13470
|
+
const readSiteAlarms = () => {
|
|
13471
|
+
const out = [];
|
|
13472
|
+
|
|
13473
|
+
alarmStorageKeys().forEach(key => {
|
|
13474
|
+
let list = [];
|
|
13475
|
+
|
|
13476
|
+
try {
|
|
13477
|
+
list = JSON.parse(localStorage.getItem(key) || '[]');
|
|
13478
|
+
} catch {
|
|
13479
|
+
list = [];
|
|
13480
|
+
}
|
|
13481
|
+
|
|
13482
|
+
(Array.isArray(list) ? list : []).forEach(item => {
|
|
13483
|
+
if (!item || typeof item !== 'object') {
|
|
13484
|
+
return;
|
|
13485
|
+
}
|
|
13486
|
+
|
|
13487
|
+
if (IS_MOBILE_SITE) {
|
|
13488
|
+
const at = Number(item.reg_date) || 0;
|
|
13489
|
+
const tag = item.id === 'hit' ? ALARM_TAGS.hit : ALARM_TAGS[item.type] || '알림';
|
|
13490
|
+
|
|
13491
|
+
out.push({
|
|
13492
|
+
key: `${item.type}:${item.id}:${item.no}:${item.comment_no || ''}:${at}`,
|
|
13493
|
+
tag,
|
|
13494
|
+
text: cleanText(item.subject).slice(0, 140) || '(내용 없음)',
|
|
13495
|
+
href: mobileAlarmHref(item),
|
|
13496
|
+
at
|
|
13497
|
+
});
|
|
13498
|
+
} else {
|
|
13499
|
+
const at = (Number(item.time) || 0) * 1000;
|
|
13500
|
+
const text = cleanText(`${item.prefix || ''} ${item.content || ''}`).replace(/<[^>]*>/g, '');
|
|
13501
|
+
const tagMatch = /^\[([^\]]{1,10})\]/.exec(cleanText(item.prefix || ''));
|
|
13502
|
+
let href = String(item.link || '');
|
|
13503
|
+
|
|
13504
|
+
if (href && !/^https?:\/\//.test(href)) {
|
|
13505
|
+
href = href.startsWith('/') ? `https://gall.dcinside.com${href}` : '';
|
|
13506
|
+
}
|
|
13507
|
+
|
|
13508
|
+
out.push({
|
|
13509
|
+
key: `${item.idx || ''}:${item.link || ''}:${at}`,
|
|
13510
|
+
tag: item.type === 'manager' ? '관리' : tagMatch ? tagMatch[1] : '알림',
|
|
13511
|
+
text: text.slice(0, 140) || '(내용 없음)',
|
|
13512
|
+
href: /^https:\/\/[a-z0-9.-]*dcinside\.com\//i.test(href) ? href : '',
|
|
13513
|
+
at,
|
|
13514
|
+
idx: item.idx
|
|
13515
|
+
});
|
|
13516
|
+
}
|
|
13517
|
+
});
|
|
13518
|
+
});
|
|
13519
|
+
|
|
13520
|
+
const seen = new Set();
|
|
13521
|
+
|
|
13522
|
+
return out
|
|
13523
|
+
.filter(item => !seen.has(item.key) && seen.add(item.key))
|
|
13524
|
+
.sort((a, b) => b.at - a.at)
|
|
13525
|
+
.slice(0, 30);
|
|
13526
|
+
};
|
|
13527
|
+
|
|
13528
|
+
const readAlarmMarks = () => {
|
|
13529
|
+
const marks = readObject(STORAGE.alarmSeen);
|
|
13530
|
+
|
|
13531
|
+
return { seen: Number(marks.seen) || 0, shown: Number(marks.shown) || 0 };
|
|
13532
|
+
};
|
|
13533
|
+
|
|
13534
|
+
const writeAlarmMarks = patch => writeJson(STORAGE.alarmSeen, { ...readAlarmMarks(), ...patch });
|
|
13535
|
+
|
|
13536
|
+
const unreadAlarmCount = () => {
|
|
13537
|
+
const { seen } = readAlarmMarks();
|
|
13538
|
+
|
|
13539
|
+
return readSiteAlarms().filter(item => item.at > seen).length;
|
|
13540
|
+
};
|
|
13541
|
+
|
|
13542
|
+
const paintAlarmBadge = () => {
|
|
13543
|
+
const fab = document.getElementById('dc-custom-fab');
|
|
13544
|
+
|
|
13545
|
+
if (!fab) {
|
|
13546
|
+
return;
|
|
13547
|
+
}
|
|
13548
|
+
|
|
13549
|
+
const count = settings.alarmBubble === false ? 0 : unreadAlarmCount();
|
|
13550
|
+
|
|
13551
|
+
if (count) {
|
|
13552
|
+
fab.dataset.dcAlarm = count > 9 ? '9+' : String(count);
|
|
13553
|
+
} else {
|
|
13554
|
+
delete fab.dataset.dcAlarm;
|
|
13555
|
+
}
|
|
13556
|
+
};
|
|
13557
|
+
|
|
13558
|
+
const markAlarmsSeen = () => {
|
|
13559
|
+
const latest = readSiteAlarms()[0];
|
|
13560
|
+
|
|
13561
|
+
writeAlarmMarks({ seen: Math.max(Date.now(), latest ? latest.at : 0) });
|
|
13562
|
+
paintAlarmBadge();
|
|
13563
|
+
};
|
|
13564
|
+
|
|
13565
|
+
let bubbleBox = null;
|
|
13566
|
+
|
|
13567
|
+
const placeBubbleBox = () => {
|
|
13568
|
+
if (!bubbleBox) {
|
|
13569
|
+
return;
|
|
13570
|
+
}
|
|
13571
|
+
|
|
13572
|
+
const fab = document.getElementById('dc-custom-fab');
|
|
13573
|
+
const rect = fab && getComputedStyle(fab).display !== 'none' ? fab.getBoundingClientRect() : null;
|
|
13574
|
+
const vw = window.innerWidth;
|
|
13575
|
+
const vh = window.innerHeight;
|
|
13576
|
+
let bottom = 144;
|
|
13577
|
+
|
|
13578
|
+
if (rect && rect.width) {
|
|
13579
|
+
const onRight = rect.left + rect.width / 2 > vw / 2;
|
|
13580
|
+
|
|
13581
|
+
bubbleBox.style.left = onRight ? '' : `${Math.max(8, rect.left)}px`;
|
|
13582
|
+
bubbleBox.style.right = onRight ? `${Math.max(8, vw - rect.right)}px` : '';
|
|
13583
|
+
bottom = Math.max(8, vh - rect.top + 10);
|
|
13584
|
+
bubbleBox.classList.toggle('dc-left', !onRight);
|
|
13585
|
+
} else {
|
|
13586
|
+
bubbleBox.style.left = '';
|
|
13587
|
+
bubbleBox.style.right = '15px';
|
|
13588
|
+
bubbleBox.classList.remove('dc-left');
|
|
13589
|
+
}
|
|
13590
|
+
|
|
13591
|
+
const toast = document.getElementById('dc-toast');
|
|
13592
|
+
|
|
13593
|
+
if (toast && toast.classList.contains('show')) {
|
|
13594
|
+
const t = toast.getBoundingClientRect();
|
|
13595
|
+
const box = bubbleBox.getBoundingClientRect();
|
|
13596
|
+
|
|
13597
|
+
if (t.width && t.left < box.right && t.right > box.left) {
|
|
13598
|
+
bottom = Math.max(bottom, vh - t.top + 8);
|
|
13599
|
+
}
|
|
13600
|
+
}
|
|
13601
|
+
|
|
13602
|
+
bubbleBox.style.bottom = `${Math.round(bottom)}px`;
|
|
13603
|
+
};
|
|
13604
|
+
|
|
13605
|
+
toastListeners.push(() => placeBubbleBox());
|
|
13606
|
+
|
|
13607
|
+
const ensureBubbleBox = () => {
|
|
13608
|
+
if (bubbleBox && bubbleBox.isConnected) {
|
|
13609
|
+
return bubbleBox;
|
|
13610
|
+
}
|
|
13611
|
+
|
|
13612
|
+
bubbleBox = h('div', 'dc-ui');
|
|
13613
|
+
bubbleBox.id = 'dc-bubbles';
|
|
13614
|
+
bubbleBox.setAttribute('role', 'log');
|
|
13615
|
+
bubbleBox.setAttribute('aria-live', 'polite');
|
|
13616
|
+
document.body.appendChild(bubbleBox);
|
|
13617
|
+
|
|
13618
|
+
return bubbleBox;
|
|
13619
|
+
};
|
|
13620
|
+
|
|
13621
|
+
const dismissBubble = (bubble, direction = 0) => {
|
|
13622
|
+
if (!bubble || bubble.classList.contains('dc-out')) {
|
|
13623
|
+
return;
|
|
13624
|
+
}
|
|
13625
|
+
|
|
13626
|
+
clearTimeout(bubble._dcTimer);
|
|
13627
|
+
bubble.classList.add('dc-out');
|
|
13628
|
+
|
|
13629
|
+
if (direction) {
|
|
13630
|
+
bubble.style.transform = `translateX(${direction * 120}%)`;
|
|
13631
|
+
}
|
|
13632
|
+
|
|
13633
|
+
setTimeout(() => bubble.remove(), 260);
|
|
13634
|
+
};
|
|
13635
|
+
|
|
13636
|
+
const openAlarm = item => {
|
|
13637
|
+
if (!IS_MOBILE_SITE && item.idx && typeof window.alarmReadOk === 'function') {
|
|
13638
|
+
try {
|
|
13639
|
+
window.alarmReadOk(item.idx);
|
|
13640
|
+
} catch {
|
|
13641
|
+
}
|
|
13642
|
+
}
|
|
13643
|
+
|
|
13644
|
+
markAlarmsSeen();
|
|
13645
|
+
|
|
13646
|
+
if (item.href) {
|
|
13647
|
+
location.href = item.href;
|
|
13648
|
+
}
|
|
13649
|
+
};
|
|
13650
|
+
|
|
13651
|
+
const bindBubbleSwipe = bubble => {
|
|
13652
|
+
let startX = 0;
|
|
13653
|
+
let dx = 0;
|
|
13654
|
+
let dragging = false;
|
|
13655
|
+
|
|
13656
|
+
bubble.addEventListener('pointerdown', event => {
|
|
13657
|
+
startX = event.clientX;
|
|
13658
|
+
dx = 0;
|
|
13659
|
+
dragging = true;
|
|
13660
|
+
clearTimeout(bubble._dcTimer);
|
|
13661
|
+
});
|
|
13662
|
+
bubble.addEventListener('pointermove', event => {
|
|
13663
|
+
if (!dragging) {
|
|
13664
|
+
return;
|
|
13665
|
+
}
|
|
13666
|
+
|
|
13667
|
+
dx = event.clientX - startX;
|
|
13668
|
+
|
|
13669
|
+
if (Math.abs(dx) > 6) {
|
|
13670
|
+
bubble.style.transition = 'none';
|
|
13671
|
+
bubble.style.transform = `translateX(${dx}px)`;
|
|
13672
|
+
bubble.style.opacity = String(Math.max(0.2, 1 - Math.abs(dx) / 220));
|
|
13673
|
+
}
|
|
13674
|
+
});
|
|
13675
|
+
|
|
13676
|
+
const end = () => {
|
|
13677
|
+
if (!dragging) {
|
|
13678
|
+
return;
|
|
13679
|
+
}
|
|
13680
|
+
|
|
13681
|
+
dragging = false;
|
|
13682
|
+
bubble.style.transition = '';
|
|
13683
|
+
|
|
13684
|
+
if (Math.abs(dx) > 70) {
|
|
13685
|
+
bubble._dcSwiped = true;
|
|
13686
|
+
dismissBubble(bubble, Math.sign(dx));
|
|
13687
|
+
} else {
|
|
13688
|
+
bubble.style.transform = '';
|
|
13689
|
+
bubble.style.opacity = '';
|
|
13690
|
+
bubble._dcTimer = setTimeout(() => dismissBubble(bubble), 6000);
|
|
13691
|
+
}
|
|
13692
|
+
};
|
|
13693
|
+
|
|
13694
|
+
bubble.addEventListener('pointerup', end);
|
|
13695
|
+
bubble.addEventListener('pointercancel', end);
|
|
13696
|
+
};
|
|
13697
|
+
|
|
13698
|
+
const showAlarmBubble = (item, extra = 0) => {
|
|
13699
|
+
const box = ensureBubbleBox();
|
|
13700
|
+
|
|
13701
|
+
placeBubbleBox();
|
|
13702
|
+
|
|
13703
|
+
while (box.children.length >= ALARM_BUBBLE_MAX) {
|
|
13704
|
+
box.firstElementChild.remove();
|
|
13705
|
+
}
|
|
13706
|
+
|
|
13707
|
+
const bubble = h('div', 'dc-bubble');
|
|
13708
|
+
const top = h('div', 'dc-bubble-top');
|
|
13709
|
+
|
|
13710
|
+
top.appendChild(h('span', 'dc-bubble-tag', item.tag));
|
|
13711
|
+
top.appendChild(h('span', 'dc-bubble-time', extra ? `새 알림 ${extra + 1}개` : timeAgo(item.at)));
|
|
13712
|
+
|
|
13713
|
+
const close = h('button', 'dc-bubble-x', '×');
|
|
13714
|
+
|
|
13715
|
+
close.type = 'button';
|
|
13716
|
+
close.setAttribute('aria-label', '알림 닫기');
|
|
13717
|
+
close.addEventListener('click', event => {
|
|
13718
|
+
event.stopPropagation();
|
|
13719
|
+
dismissBubble(bubble);
|
|
13720
|
+
});
|
|
13721
|
+
top.appendChild(close);
|
|
13722
|
+
|
|
13723
|
+
bubble.appendChild(top);
|
|
13724
|
+
bubble.appendChild(h('div', 'dc-bubble-text', item.text));
|
|
13725
|
+
|
|
13726
|
+
if (extra) {
|
|
13727
|
+
bubble.appendChild(h('div', 'dc-bubble-more', `외 ${extra}개 · 눌러서 모두 보기`));
|
|
13728
|
+
}
|
|
13729
|
+
|
|
13730
|
+
bubble.addEventListener('click', () => {
|
|
13731
|
+
if (bubble._dcSwiped) {
|
|
13732
|
+
return;
|
|
13733
|
+
}
|
|
13734
|
+
|
|
13735
|
+
dismissBubble(bubble);
|
|
13736
|
+
|
|
13737
|
+
if (extra) {
|
|
13738
|
+
openBlockModal('recent');
|
|
13739
|
+
} else {
|
|
13740
|
+
openAlarm(item);
|
|
13741
|
+
}
|
|
13742
|
+
});
|
|
13743
|
+
|
|
13744
|
+
bindBubbleSwipe(bubble);
|
|
13745
|
+
box.appendChild(bubble);
|
|
13746
|
+
requestAnimationFrame(() => bubble.classList.add('dc-in'));
|
|
13747
|
+
bubble._dcTimer = setTimeout(() => dismissBubble(bubble), 8000);
|
|
13748
|
+
};
|
|
13749
|
+
|
|
13750
|
+
let alarmRawMemo = null;
|
|
13751
|
+
let alarmWatchTimer = null;
|
|
13752
|
+
|
|
13753
|
+
const checkSiteAlarms = () => {
|
|
13754
|
+
if (settings.alarmBubble === false || document.visibilityState === 'hidden') {
|
|
13755
|
+
return;
|
|
13756
|
+
}
|
|
13757
|
+
|
|
13758
|
+
const raw = readAlarmRaw();
|
|
13759
|
+
|
|
13760
|
+
if (raw === alarmRawMemo) {
|
|
13761
|
+
return;
|
|
13762
|
+
}
|
|
13763
|
+
|
|
13764
|
+
alarmRawMemo = raw;
|
|
13765
|
+
|
|
13766
|
+
const list = readSiteAlarms();
|
|
13767
|
+
const marks = readAlarmMarks();
|
|
13768
|
+
const latest = list.length ? list[0].at : 0;
|
|
13769
|
+
|
|
13770
|
+
if (!marks.shown) {
|
|
13771
|
+
writeAlarmMarks({ shown: Math.max(latest, Date.now() - ALARM_BUBBLE_FRESH), seen: marks.seen || latest });
|
|
13772
|
+
paintAlarmBadge();
|
|
13773
|
+
return;
|
|
13774
|
+
}
|
|
13775
|
+
|
|
13776
|
+
const fresh = list.filter(item => item.at > marks.shown && Date.now() - item.at < ALARM_BUBBLE_FRESH);
|
|
13777
|
+
|
|
13778
|
+
if (latest > marks.shown) {
|
|
13779
|
+
writeAlarmMarks({ shown: latest });
|
|
13780
|
+
}
|
|
13781
|
+
|
|
13782
|
+
if (fresh.length) {
|
|
13783
|
+
showAlarmBubble(fresh[0], fresh.length > ALARM_BUBBLE_MAX ? fresh.length - 1 : 0);
|
|
13784
|
+
|
|
13785
|
+
if (fresh.length > 1 && fresh.length <= ALARM_BUBBLE_MAX) {
|
|
13786
|
+
fresh.slice(1).reverse().forEach(item => showAlarmBubble(item));
|
|
13787
|
+
}
|
|
13788
|
+
}
|
|
13789
|
+
|
|
13790
|
+
paintAlarmBadge();
|
|
13791
|
+
};
|
|
13792
|
+
|
|
13793
|
+
let alarmPollAt = Date.now();
|
|
13794
|
+
|
|
13795
|
+
const siteAlarmsOff = () => {
|
|
13796
|
+
try {
|
|
13797
|
+
const list = JSON.parse(localStorage.getItem('alarm_setting') || '[]');
|
|
13798
|
+
|
|
13799
|
+
return Array.isArray(list) && list.some(item => item && item.setting === 'onoff' && item.value !== 'on');
|
|
13800
|
+
} catch {
|
|
13801
|
+
return false;
|
|
13802
|
+
}
|
|
13803
|
+
};
|
|
13804
|
+
|
|
13805
|
+
const keepSiteAlarmPolling = () => {
|
|
13806
|
+
if (!IS_MOBILE_SITE || settings.alarmKeepPolling === false || settings.alarmBubble === false) {
|
|
13807
|
+
return;
|
|
13808
|
+
}
|
|
13809
|
+
|
|
13810
|
+
if (document.visibilityState === 'hidden' || !siteUserId() || siteAlarmsOff() || netPausedUntil()) {
|
|
13811
|
+
return;
|
|
13812
|
+
}
|
|
13813
|
+
|
|
13814
|
+
if (Date.now() - alarmPollAt < ALARM_POLL_GAP || typeof window.alarm_notification_pop !== 'function') {
|
|
13815
|
+
return;
|
|
13816
|
+
}
|
|
13817
|
+
|
|
13818
|
+
alarmPollAt = Date.now();
|
|
13819
|
+
|
|
13820
|
+
try {
|
|
13821
|
+
if (typeof window.set_interval_noti === 'number' && window.set_interval_noti >= 1) {
|
|
13822
|
+
window.set_interval_noti = 1;
|
|
13823
|
+
}
|
|
13824
|
+
|
|
13825
|
+
window.alarm_notification_pop();
|
|
13826
|
+
} catch {
|
|
13827
|
+
}
|
|
13828
|
+
};
|
|
13829
|
+
|
|
13830
|
+
function initSiteAlarms() {
|
|
13831
|
+
if (alarmWatchTimer || !(IS_MOBILE_SITE || /^gall\.dcinside\.com$/i.test(location.hostname))) {
|
|
13832
|
+
paintAlarmBadge();
|
|
13833
|
+
return;
|
|
13834
|
+
}
|
|
13835
|
+
|
|
13836
|
+
if (!siteUserId() && !alarmStorageKeys().length) {
|
|
13837
|
+
paintAlarmBadge();
|
|
13838
|
+
|
|
13839
|
+
if (!initSiteAlarms.retried) {
|
|
13840
|
+
initSiteAlarms.retried = true;
|
|
13841
|
+
[3000, 10000].forEach(delay => setTimeout(() => {
|
|
13842
|
+
if (!alarmWatchTimer && (siteUserId() || alarmStorageKeys().length)) {
|
|
13843
|
+
initSiteAlarms();
|
|
13844
|
+
}
|
|
13845
|
+
}, delay));
|
|
13846
|
+
}
|
|
13847
|
+
|
|
13848
|
+
return;
|
|
13849
|
+
}
|
|
13850
|
+
|
|
13851
|
+
checkSiteAlarms();
|
|
13852
|
+
alarmWatchTimer = setInterval(() => {
|
|
13853
|
+
checkSiteAlarms();
|
|
13854
|
+
keepSiteAlarmPolling();
|
|
13855
|
+
}, 2000);
|
|
13856
|
+
|
|
13857
|
+
document.addEventListener('visibilitychange', () => {
|
|
13858
|
+
if (document.visibilityState === 'visible') {
|
|
13859
|
+
checkSiteAlarms();
|
|
13860
|
+
paintAlarmBadge();
|
|
13861
|
+
}
|
|
13862
|
+
});
|
|
13863
|
+
|
|
13864
|
+
window.addEventListener('storage', event => {
|
|
13865
|
+
if (event.key === STORAGE.alarmSeen || /^alarm_(list|notification_)/.test(event.key || '')) {
|
|
13866
|
+
alarmRawMemo = null;
|
|
13867
|
+
checkSiteAlarms();
|
|
13868
|
+
paintAlarmBadge();
|
|
13869
|
+
}
|
|
13870
|
+
});
|
|
13871
|
+
|
|
13872
|
+
window.addEventListener('resize', placeBubbleBox, { passive: true });
|
|
13873
|
+
};
|
|
13874
|
+
|
|
13875
|
+
// =========================================================
|
|
13876
|
+
|
|
13877
|
+
// =========================================================
|
|
13878
|
+
|
|
13879
|
+
const renderRecentView = area => {
|
|
13880
|
+
const alarms = siteUserId() || alarmStorageKeys().length ? readSiteAlarms() : null;
|
|
13881
|
+
|
|
13882
|
+
if (alarms) {
|
|
13883
|
+
const { seen } = readAlarmMarks();
|
|
13884
|
+
const unread = alarms.filter(item => item.at > seen).length;
|
|
13885
|
+
|
|
13886
|
+
area.appendChild(makeSectionTitle(`🔔 알림${unread ? ` · 새 알림 ${unread}개` : ''}`));
|
|
13887
|
+
|
|
13888
|
+
if (!alarms.length) {
|
|
13889
|
+
area.appendChild(makeEmpty('받은 알림이 없습니다. 댓글·답글 등 디시 알림이 오면 여기와 플로팅 버튼 곁 말풍선으로 보입니다.'));
|
|
13890
|
+
} else {
|
|
13891
|
+
alarms.slice(0, 15).forEach(item => {
|
|
13892
|
+
const row = makeRow({
|
|
13893
|
+
title: item.text,
|
|
13894
|
+
chip: item.tag,
|
|
13895
|
+
chipClass: item.at > seen ? 'on' : '',
|
|
13896
|
+
meta: timeAgo(item.at),
|
|
13897
|
+
actions: item.href ? [{ label: '열기', href: item.href }] : []
|
|
13898
|
+
});
|
|
13899
|
+
|
|
13900
|
+
if (item.at > seen) {
|
|
13901
|
+
row.classList.add('dc-row-new');
|
|
13902
|
+
}
|
|
13903
|
+
|
|
13904
|
+
if (item.href) {
|
|
13905
|
+
row.querySelector('a.dc-btn')?.addEventListener('click', () => openAlarm({ ...item, href: '' }));
|
|
13906
|
+
}
|
|
13907
|
+
|
|
13908
|
+
area.appendChild(row);
|
|
13909
|
+
});
|
|
13910
|
+
|
|
13911
|
+
if (unread) {
|
|
13912
|
+
area.appendChild(makeButton('✓ 모두 읽음', 'wide', () => {
|
|
13913
|
+
markAlarmsSeen();
|
|
13914
|
+
rerenderModal();
|
|
13915
|
+
}));
|
|
13916
|
+
}
|
|
13917
|
+
}
|
|
13918
|
+
} else if (IS_MOBILE_SITE || /^gall\.dcinside\.com$/i.test(location.hostname)) {
|
|
13919
|
+
area.appendChild(makeSectionTitle('🔔 알림'));
|
|
13920
|
+
area.appendChild(makeNote('디시에 로그인하면 댓글·답글 알림을 여기와 플로팅 말풍선으로 볼 수 있습니다.'));
|
|
13921
|
+
}
|
|
13922
|
+
|
|
13923
|
+
area.appendChild(makeSectionTitle(`🕘 최근 방문 갤러리 · ${recentGalls.length}개`));
|
|
13924
|
+
|
|
13925
|
+
const syncOn = typeof syncReady === 'function' && syncReady() && !readSyncConf().skip.includes('recent');
|
|
13926
|
+
|
|
13927
|
+
area.appendChild(makeNote(syncOn
|
|
13928
|
+
? '동기화로 모든 기기의 방문 기록이 합쳐집니다. 디시 화면의 최근 방문 목록에도 함께 보입니다.'
|
|
13929
|
+
: '디시의 최근 방문 목록과 같은 내용입니다. 동기화를 켜면 다른 기기의 방문도 합쳐집니다.'));
|
|
13930
|
+
|
|
13931
|
+
if (!recentGalls.length) {
|
|
13932
|
+
area.appendChild(makeEmpty('아직 방문한 갤러리가 없습니다.'));
|
|
13933
|
+
return;
|
|
13934
|
+
}
|
|
13935
|
+
|
|
13936
|
+
const filterRow = makeFilterRow(recentGalls.length);
|
|
13937
|
+
|
|
13938
|
+
if (filterRow) {
|
|
13939
|
+
area.appendChild(filterRow);
|
|
13940
|
+
}
|
|
13941
|
+
|
|
13942
|
+
recentGalls.forEach(item => {
|
|
13943
|
+
area.appendChild(makeRow({
|
|
13944
|
+
title: item.n || item.id,
|
|
13945
|
+
chip: RECENT_KIND_LABEL[item.k] || '',
|
|
13946
|
+
meta: [item.id, item.at ? timeAgo(item.at) : ''].filter(Boolean).join(' · '),
|
|
13947
|
+
actions: [
|
|
13948
|
+
{ label: '열기', href: recentUrl(item) },
|
|
13949
|
+
{
|
|
13950
|
+
label: '지우기',
|
|
13951
|
+
onClick: () => {
|
|
13952
|
+
recentGalls = recentGalls.filter(other => other !== item);
|
|
13953
|
+
saveRecent();
|
|
13954
|
+
exportNativeRecent();
|
|
13955
|
+
rerenderModal();
|
|
13956
|
+
}
|
|
13957
|
+
}
|
|
13958
|
+
]
|
|
13959
|
+
}));
|
|
13960
|
+
});
|
|
13961
|
+
|
|
13962
|
+
area.appendChild(makeConfirmButton('최근 방문 모두 지우기', '최근 방문 갤러리 목록을 모두 지울까요? (동기화 중이면 다른 기기에서도 지워집니다)', () => {
|
|
13963
|
+
recentGalls = [];
|
|
13964
|
+
saveRecent();
|
|
13965
|
+
exportNativeRecent();
|
|
13966
|
+
rerenderModal();
|
|
13967
|
+
}));
|
|
13968
|
+
};
|
|
13969
|
+
|
|
13970
|
+
(() => {
|
|
13971
|
+
const settingsView = MODAL_VIEWS.settings;
|
|
13972
|
+
|
|
13973
|
+
delete MODAL_VIEWS.settings;
|
|
13974
|
+
MODAL_VIEWS.recent = {
|
|
13975
|
+
title: '알림 · 최근 갤러리',
|
|
13976
|
+
tab: '🕘 최근',
|
|
13977
|
+
render: area => {
|
|
13978
|
+
document.querySelectorAll('#dc-bubbles .dc-bubble').forEach(bubble => dismissBubble(bubble));
|
|
13979
|
+
renderRecentView(area);
|
|
13980
|
+
|
|
13981
|
+
setTimeout(markAlarmsSeen, 600);
|
|
13982
|
+
}
|
|
13983
|
+
};
|
|
13984
|
+
MODAL_VIEWS.settings = settingsView;
|
|
13985
|
+
})();
|
|
13986
|
+
|
|
13987
|
+
initRecentEarly();
|
|
13988
|
+
|
|
13989
|
+
// =========================================================
|
|
13990
|
+
|
|
13991
|
+
// =========================================================
|
|
13992
|
+
|
|
13993
|
+
const KEY_HELP = 'J/K 다음·이전 글 · Enter 열기 · S 스크랩 · / 검색 · B 차단 관리';
|
|
13994
|
+
|
|
13995
|
+
const typingTarget = el =>
|
|
13996
|
+
!!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || !!el.closest?.('[contenteditable="true"]'));
|
|
13997
|
+
|
|
13998
|
+
const visibleListRows = () => {
|
|
13999
|
+
const listEl = pickListElement(document);
|
|
14000
|
+
|
|
14001
|
+
if (!listEl) {
|
|
14002
|
+
return [];
|
|
14003
|
+
}
|
|
14004
|
+
|
|
14005
|
+
return realListItems(listEl).filter(row => row.getClientRects().length && listItemKey(row) && !row.closest('.dc-ui'));
|
|
14006
|
+
};
|
|
14007
|
+
|
|
14008
|
+
let keyCursor = null;
|
|
14009
|
+
|
|
14010
|
+
const moveKeyCursor = step => {
|
|
14011
|
+
const rows = visibleListRows();
|
|
14012
|
+
|
|
14013
|
+
if (!rows.length) {
|
|
14014
|
+
return false;
|
|
14015
|
+
}
|
|
14016
|
+
|
|
14017
|
+
const here = currentPostKey();
|
|
14018
|
+
let index = keyCursor ? rows.indexOf(keyCursor) : -1;
|
|
14019
|
+
|
|
14020
|
+
if (currentPostInfo()) {
|
|
14021
|
+
const at = rows.findIndex(row => listItemKey(row) === here);
|
|
14022
|
+
const target = at >= 0 ? rows[at + step] : step > 0 ? rows[0] : null;
|
|
14023
|
+
const link = target && target.querySelector(POST_LINK_SELECTOR);
|
|
14024
|
+
|
|
14025
|
+
if (link) {
|
|
14026
|
+
location.href = link.href;
|
|
14027
|
+
return true;
|
|
14028
|
+
}
|
|
14029
|
+
|
|
14030
|
+
showToast(step > 0 ? '아래에 더 볼 글이 없습니다' : '첫 글입니다');
|
|
14031
|
+
return true;
|
|
14032
|
+
}
|
|
14033
|
+
|
|
14034
|
+
if (index < 0) {
|
|
14035
|
+
index = rows.findIndex(row => row.getBoundingClientRect().bottom > 60);
|
|
14036
|
+
index = Math.max(0, step > 0 ? index : index);
|
|
14037
|
+
} else {
|
|
14038
|
+
index = Math.min(rows.length - 1, Math.max(0, index + step));
|
|
14039
|
+
}
|
|
14040
|
+
|
|
14041
|
+
keyCursor?.classList.remove('dc-key-cursor');
|
|
14042
|
+
keyCursor = rows[index];
|
|
14043
|
+
keyCursor.classList.add('dc-key-cursor');
|
|
14044
|
+
keyCursor.scrollIntoView({ block: 'nearest' });
|
|
14045
|
+
|
|
14046
|
+
return true;
|
|
14047
|
+
};
|
|
14048
|
+
|
|
14049
|
+
const initKeyboardShortcuts = () => {
|
|
14050
|
+
document.addEventListener('keydown', event => {
|
|
14051
|
+
if (settings.keyboardShortcuts === false || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
|
|
14052
|
+
return;
|
|
14053
|
+
}
|
|
14054
|
+
|
|
14055
|
+
if (typingTarget(event.target) || typingTarget(document.activeElement)) {
|
|
14056
|
+
return;
|
|
14057
|
+
}
|
|
14058
|
+
|
|
14059
|
+
if (document.getElementById('dc-custom-modal')?.classList.contains('show')) {
|
|
14060
|
+
return;
|
|
14061
|
+
}
|
|
14062
|
+
|
|
14063
|
+
const key = event.key;
|
|
14064
|
+
let handled = false;
|
|
14065
|
+
|
|
14066
|
+
if (key === 'j' || key === 'J') {
|
|
14067
|
+
handled = moveKeyCursor(1);
|
|
14068
|
+
} else if (key === 'k' || key === 'K') {
|
|
14069
|
+
handled = moveKeyCursor(-1);
|
|
14070
|
+
} else if ((key === 'Enter' || key === 'o' || key === 'O') && keyCursor && keyCursor.isConnected) {
|
|
14071
|
+
const link = keyCursor.querySelector(POST_LINK_SELECTOR);
|
|
14072
|
+
|
|
14073
|
+
if (link) {
|
|
14074
|
+
location.href = link.href;
|
|
14075
|
+
handled = true;
|
|
14076
|
+
}
|
|
14077
|
+
} else if (key === 's' || key === 'S') {
|
|
14078
|
+
const info = currentPostInfo();
|
|
14079
|
+
|
|
14080
|
+
if (info) {
|
|
14081
|
+
toggleScrap(info);
|
|
14082
|
+
handled = true;
|
|
14083
|
+
}
|
|
14084
|
+
} else if (key === '/') {
|
|
14085
|
+
const box = document.querySelector('.in_keyword, #preSWord, input.js-keyword, input[name="search_keyword"], .top_search input[type="text"]');
|
|
14086
|
+
|
|
14087
|
+
if (box) {
|
|
14088
|
+
box.focus();
|
|
14089
|
+
handled = true;
|
|
14090
|
+
}
|
|
14091
|
+
} else if (key === 'b' || key === 'B') {
|
|
14092
|
+
openBlockModal(document.getElementById('dc-custom-fab')?.dataset.dcAlarm ? 'recent' : 'overview');
|
|
14093
|
+
handled = true;
|
|
14094
|
+
} else if (key === '?') {
|
|
14095
|
+
showToast(`⌨️ ${KEY_HELP}`, { duration: 6000 });
|
|
14096
|
+
handled = true;
|
|
14097
|
+
}
|
|
14098
|
+
|
|
14099
|
+
if (handled) {
|
|
14100
|
+
event.preventDefault();
|
|
14101
|
+
}
|
|
14102
|
+
});
|
|
14103
|
+
};
|
|
14104
|
+
|
|
14105
|
+
// =========================================================
|
|
14106
|
+
|
|
14107
|
+
// =========================================================
|
|
14108
|
+
|
|
14109
|
+
const SEARCH_HISTORY_MAX = 30;
|
|
14110
|
+
|
|
14111
|
+
const readSiteSearchHistory = () => {
|
|
14112
|
+
if (!IS_MOBILE_SITE) {
|
|
14113
|
+
return null;
|
|
14114
|
+
}
|
|
14115
|
+
|
|
14116
|
+
try {
|
|
14117
|
+
const list = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
|
14118
|
+
|
|
14119
|
+
return Array.isArray(list) ? list.filter(item => typeof item === 'string' && item.trim()).map(item => item.slice(0, 60)) : [];
|
|
14120
|
+
} catch {
|
|
14121
|
+
return [];
|
|
14122
|
+
}
|
|
14123
|
+
};
|
|
14124
|
+
|
|
14125
|
+
const collectSearchForSync = () => {
|
|
14126
|
+
const site = readSiteSearchHistory();
|
|
14127
|
+
const list = site || readArray(STORAGE.searchHistory).filter(item => typeof item === 'string');
|
|
14128
|
+
|
|
14129
|
+
return [...new Set(list)].slice(-SEARCH_HISTORY_MAX);
|
|
14130
|
+
};
|
|
14131
|
+
|
|
14132
|
+
const mergeSearchHistory = (local = [], base, remote) => {
|
|
14133
|
+
if (!Array.isArray(remote)) {
|
|
14134
|
+
return [...new Set(local || [])].slice(-SEARCH_HISTORY_MAX);
|
|
14135
|
+
}
|
|
14136
|
+
|
|
14137
|
+
return mergeList(local || [], Array.isArray(base) ? base : null, remote).slice(-SEARCH_HISTORY_MAX);
|
|
14138
|
+
};
|
|
14139
|
+
|
|
14140
|
+
const applySearchFromSync = list => {
|
|
14141
|
+
if (!Array.isArray(list)) {
|
|
14142
|
+
return false;
|
|
14143
|
+
}
|
|
14144
|
+
|
|
14145
|
+
const next = [...new Set(list.filter(item => typeof item === 'string'))].slice(-SEARCH_HISTORY_MAX);
|
|
14146
|
+
|
|
14147
|
+
if (JSON.stringify(next) === JSON.stringify(collectSearchForSync())) {
|
|
14148
|
+
return false;
|
|
14149
|
+
}
|
|
14150
|
+
|
|
14151
|
+
writeJson(STORAGE.searchHistory, next);
|
|
14152
|
+
|
|
14153
|
+
if (IS_MOBILE_SITE) {
|
|
14154
|
+
try {
|
|
14155
|
+
localStorage.setItem('searchHistory', JSON.stringify(next));
|
|
14156
|
+
} catch {
|
|
14157
|
+
}
|
|
14158
|
+
}
|
|
14159
|
+
|
|
14160
|
+
return true;
|
|
14161
|
+
};
|
|
12578
14162
|
// =========================================================
|
|
12579
14163
|
// 14. 플로팅 버튼
|
|
12580
14164
|
// =========================================================
|
|
@@ -12892,7 +14476,7 @@
|
|
|
12892
14476
|
return;
|
|
12893
14477
|
}
|
|
12894
14478
|
|
|
12895
|
-
openBlockModal();
|
|
14479
|
+
openBlockModal(fab.dataset.dcAlarm ? 'recent' : 'overview');
|
|
12896
14480
|
});
|
|
12897
14481
|
|
|
12898
14482
|
wake();
|
|
@@ -13533,6 +15117,7 @@
|
|
|
13533
15117
|
|
|
13534
15118
|
state.strikes = Date.now() - state.lastStrike < 60 * 60 * 1000 ? state.strikes + 1 : 1;
|
|
13535
15119
|
state.lastStrike = Date.now();
|
|
15120
|
+
state.src = 'page';
|
|
13536
15121
|
state.until = Date.now() + Math.min(NET_COOLDOWN_MAX, NET_COOLDOWN * 2 ** (state.strikes - 1));
|
|
13537
15122
|
writeNetState(state);
|
|
13538
15123
|
}
|
|
@@ -13559,6 +15144,9 @@
|
|
|
13559
15144
|
initScrap();
|
|
13560
15145
|
initScrollIndicator();
|
|
13561
15146
|
initSync();
|
|
15147
|
+
initRecent();
|
|
15148
|
+
initSiteAlarms();
|
|
15149
|
+
initKeyboardShortcuts();
|
|
13562
15150
|
restoreListSnapshot();
|
|
13563
15151
|
processElements();
|
|
13564
15152
|
|