@kangsicj/dcinsideenhancer 1.1.0 → 1.2.0
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 +5 -1
- package/README.md +5 -1
- package/dist/DCInsideEnhancer.meta.js +1 -1
- package/dist/DCInsideEnhancer.user.js +1307 -22
- 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.2.0
|
|
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.2.0';
|
|
79
79
|
const PROJECT_URL = 'https://github.com/kangsicj/DCInsideEnhancer';
|
|
80
80
|
const DONATE_URL = 'https://fairy.hada.io/@jinny#support';
|
|
81
81
|
|
|
@@ -107,6 +107,8 @@
|
|
|
107
107
|
syncRt: 'dc_sync_rt',
|
|
108
108
|
recent: 'dc_recent_galls',
|
|
109
109
|
recentNative: 'dc_recent_native',
|
|
110
|
+
searchHistory: 'dc_search_history',
|
|
111
|
+
blockHits: 'dc_block_hits',
|
|
110
112
|
alarmSeen: 'dc_alarm_marks',
|
|
111
113
|
seenVersion: 'dc_seen_version'
|
|
112
114
|
};
|
|
@@ -222,6 +224,9 @@
|
|
|
222
224
|
hideSilbeAlarm: true,
|
|
223
225
|
alarmBubble: true,
|
|
224
226
|
alarmKeepPolling: true,
|
|
227
|
+
keyboardShortcuts: true,
|
|
228
|
+
hoverPreview: true,
|
|
229
|
+
alarmSystemNotify: false,
|
|
225
230
|
pcFitWidth: true,
|
|
226
231
|
autoHideScrollbar: true,
|
|
227
232
|
scrapIntercept: true,
|
|
@@ -275,6 +280,8 @@
|
|
|
275
280
|
--dc-type-id: #6d538d;
|
|
276
281
|
--dc-shadow: 0 8px 24px rgba(0,0,0,.16);
|
|
277
282
|
--dc-shadow-sm: 0 2px 8px rgba(0,0,0,.12);
|
|
283
|
+
--dc-raised: #ffffff;
|
|
284
|
+
--dc-raised-shadow: 0 1px 2px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.08);
|
|
278
285
|
--dc-overlay: rgba(0,0,0,.45);
|
|
279
286
|
--dc-ghost-bg: rgba(128,128,128,.12);
|
|
280
287
|
--dc-ghost-border: rgba(128,128,128,.38);
|
|
@@ -300,6 +307,8 @@
|
|
|
300
307
|
--dc-type-id: #9c86b1;
|
|
301
308
|
--dc-shadow: 0 8px 28px rgba(0,0,0,.55);
|
|
302
309
|
--dc-shadow-sm: 0 2px 10px rgba(0,0,0,.45);
|
|
310
|
+
--dc-raised: #383b44;
|
|
311
|
+
--dc-raised-shadow: inset 0 0 0 1px rgba(255,255,255,.07), 0 1px 2px rgba(0,0,0,.3);
|
|
303
312
|
--dc-overlay: rgba(0,0,0,.6);
|
|
304
313
|
--dc-ghost-bg: rgba(150,150,150,.11);
|
|
305
314
|
--dc-ghost-border: rgba(150,150,150,.30);
|
|
@@ -949,6 +958,10 @@
|
|
|
949
958
|
|
|
950
959
|
#dc-custom-modal .dc-head-btn {
|
|
951
960
|
flex: 0 0 auto;
|
|
961
|
+
display: inline-flex;
|
|
962
|
+
align-items: center;
|
|
963
|
+
justify-content: center;
|
|
964
|
+
box-sizing: border-box;
|
|
952
965
|
height: 30px;
|
|
953
966
|
padding: 0 12px;
|
|
954
967
|
border: 1px solid var(--dc-border);
|
|
@@ -957,7 +970,7 @@
|
|
|
957
970
|
color: var(--dc-text-dim);
|
|
958
971
|
font-size: 12px;
|
|
959
972
|
font-weight: 700;
|
|
960
|
-
line-height:
|
|
973
|
+
line-height: 1;
|
|
961
974
|
cursor: pointer;
|
|
962
975
|
}
|
|
963
976
|
|
|
@@ -978,7 +991,15 @@
|
|
|
978
991
|
|
|
979
992
|
#dc-custom-modal .dc-modal-tab {
|
|
980
993
|
flex: 1 1 0;
|
|
994
|
+
display: flex;
|
|
995
|
+
align-items: center;
|
|
996
|
+
justify-content: center;
|
|
997
|
+
gap: 3px;
|
|
998
|
+
min-width: 0;
|
|
999
|
+
box-sizing: border-box;
|
|
981
1000
|
height: 36px;
|
|
1001
|
+
margin: 0;
|
|
1002
|
+
line-height: 1;
|
|
982
1003
|
padding: 0 2px;
|
|
983
1004
|
border: none;
|
|
984
1005
|
border-radius: 9px;
|
|
@@ -992,11 +1013,14 @@
|
|
|
992
1013
|
}
|
|
993
1014
|
|
|
994
1015
|
#dc-custom-modal .dc-modal-tab.active {
|
|
995
|
-
background: var(--dc-
|
|
1016
|
+
background: var(--dc-raised);
|
|
996
1017
|
color: var(--dc-accent-ink);
|
|
997
|
-
box-shadow: var(--dc-shadow
|
|
1018
|
+
box-shadow: var(--dc-raised-shadow);
|
|
998
1019
|
}
|
|
999
1020
|
|
|
1021
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-ico { flex: 0 0 auto; font-size: 13px; line-height: 1; }
|
|
1022
|
+
#dc-custom-modal .dc-modal-tab .dc-tab-txt { min-width: 0; overflow: hidden; text-overflow: ellipsis; line-height: 1.2; }
|
|
1023
|
+
|
|
1000
1024
|
#dc-custom-modal .dc-section-title {
|
|
1001
1025
|
display: flex;
|
|
1002
1026
|
align-items: center;
|
|
@@ -1830,6 +1854,161 @@
|
|
|
1830
1854
|
|
|
1831
1855
|
#dc-custom-modal .dc-row.dc-row-new .dc-row-title { font-weight: 800; }
|
|
1832
1856
|
|
|
1857
|
+
#dc-preview {
|
|
1858
|
+
position: fixed;
|
|
1859
|
+
z-index: 99997;
|
|
1860
|
+
display: flex;
|
|
1861
|
+
flex-direction: column;
|
|
1862
|
+
box-sizing: border-box;
|
|
1863
|
+
border: 1px solid var(--dc-border);
|
|
1864
|
+
border-radius: 14px;
|
|
1865
|
+
background: var(--dc-surface);
|
|
1866
|
+
color: var(--dc-text);
|
|
1867
|
+
box-shadow: var(--dc-shadow);
|
|
1868
|
+
font-size: 14px;
|
|
1869
|
+
line-height: 1.65;
|
|
1870
|
+
opacity: 0;
|
|
1871
|
+
visibility: hidden;
|
|
1872
|
+
pointer-events: none;
|
|
1873
|
+
transform: translateY(4px);
|
|
1874
|
+
transition: opacity .15s ease, transform .15s ease, visibility 0s linear .15s;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
#dc-preview.show {
|
|
1878
|
+
opacity: 1;
|
|
1879
|
+
visibility: visible;
|
|
1880
|
+
pointer-events: auto;
|
|
1881
|
+
transform: none;
|
|
1882
|
+
transition: opacity .15s ease, transform .15s ease, visibility 0s;
|
|
1883
|
+
}
|
|
1884
|
+
|
|
1885
|
+
#dc-preview .dc-pv-head {
|
|
1886
|
+
flex: 0 0 auto;
|
|
1887
|
+
padding: 14px 16px 10px;
|
|
1888
|
+
border-bottom: 1px solid var(--dc-border);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
#dc-preview .dc-pv-title {
|
|
1892
|
+
font-size: 15px;
|
|
1893
|
+
font-weight: 800;
|
|
1894
|
+
line-height: 1.4;
|
|
1895
|
+
word-break: break-all;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
#dc-preview .dc-pv-meta {
|
|
1899
|
+
margin-top: 3px;
|
|
1900
|
+
color: var(--dc-text-dim);
|
|
1901
|
+
font-size: 12px;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
#dc-preview .dc-pv-actions {
|
|
1905
|
+
display: flex;
|
|
1906
|
+
flex-wrap: wrap;
|
|
1907
|
+
align-items: center;
|
|
1908
|
+
gap: 6px;
|
|
1909
|
+
margin-top: 8px;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
#dc-preview .dc-btn {
|
|
1913
|
+
display: inline-flex;
|
|
1914
|
+
align-items: center;
|
|
1915
|
+
justify-content: center;
|
|
1916
|
+
box-sizing: border-box;
|
|
1917
|
+
height: 28px;
|
|
1918
|
+
padding: 0 11px;
|
|
1919
|
+
border: 1px solid var(--dc-border);
|
|
1920
|
+
border-radius: 999px;
|
|
1921
|
+
background: var(--dc-surface-2);
|
|
1922
|
+
color: var(--dc-text);
|
|
1923
|
+
font-size: 12px;
|
|
1924
|
+
font-weight: 700;
|
|
1925
|
+
line-height: 1;
|
|
1926
|
+
text-decoration: none;
|
|
1927
|
+
cursor: pointer;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
#dc-preview .dc-btn:hover { border-color: var(--dc-accent-ink); color: var(--dc-accent-ink); }
|
|
1931
|
+
#dc-preview .dc-btn.danger-ghost { color: var(--dc-danger-ink); }
|
|
1932
|
+
#dc-preview .dc-btn.dc-pv-close { margin-left: auto; width: 28px; padding: 0; }
|
|
1933
|
+
|
|
1934
|
+
#dc-preview .dc-pv-body {
|
|
1935
|
+
flex: 1 1 auto;
|
|
1936
|
+
min-height: 60px;
|
|
1937
|
+
padding: 12px 16px 18px;
|
|
1938
|
+
overflow-y: auto;
|
|
1939
|
+
overscroll-behavior: contain;
|
|
1940
|
+
word-break: break-word;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
#dc-preview .dc-pv-body img,
|
|
1944
|
+
#dc-preview .dc-pv-body video { max-width: 100% !important; height: auto !important; border-radius: 6px; }
|
|
1945
|
+
#dc-preview .dc-pv-body p { margin: 0 0 4px; }
|
|
1946
|
+
#dc-preview .dc-pv-body { overflow-x: hidden; overflow-wrap: anywhere; }
|
|
1947
|
+
#dc-preview .dc-pv-body .write_div,
|
|
1948
|
+
#dc-preview .dc-pv-body [style*="width"] { width: auto !important; max-width: 100% !important; }
|
|
1949
|
+
#dc-preview .dc-pv-body * { white-space: normal !important; }
|
|
1950
|
+
#dc-preview .dc-pv-body pre, #dc-preview .dc-pv-body pre * { white-space: pre-wrap !important; }
|
|
1951
|
+
#dc-preview .dc-pv-body a { color: var(--dc-accent-ink); }
|
|
1952
|
+
#dc-preview .dc-pv-body table { max-width: 100%; }
|
|
1953
|
+
|
|
1954
|
+
html.dc-dark #dc-preview .dc-pv-body [style*="background"] { background: transparent !important; }
|
|
1955
|
+
html.dc-dark #dc-preview .dc-pv-body [style*="color: rgb(0, 0, 0)"],
|
|
1956
|
+
html.dc-dark #dc-preview .dc-pv-body [style*="color:#000"],
|
|
1957
|
+
html.dc-dark #dc-preview .dc-pv-body [style*="color: #000"],
|
|
1958
|
+
html.dc-dark #dc-preview .dc-pv-body [style*="color:black"],
|
|
1959
|
+
html.dc-dark #dc-preview .dc-pv-body font[color="#000000"] { color: inherit !important; }
|
|
1960
|
+
|
|
1961
|
+
#dc-preview .dc-pv-loading,
|
|
1962
|
+
#dc-preview .dc-pv-note {
|
|
1963
|
+
padding: 10px 0;
|
|
1964
|
+
color: var(--dc-text-dim);
|
|
1965
|
+
font-size: 13px;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
#dc-preview .dc-pv-loading::before {
|
|
1969
|
+
content: '';
|
|
1970
|
+
display: inline-block;
|
|
1971
|
+
width: 12px;
|
|
1972
|
+
height: 12px;
|
|
1973
|
+
margin-right: 8px;
|
|
1974
|
+
border: 2px solid var(--dc-border);
|
|
1975
|
+
border-top-color: var(--dc-accent-ink);
|
|
1976
|
+
border-radius: 50%;
|
|
1977
|
+
vertical-align: -1px;
|
|
1978
|
+
animation: dc-spin .7s linear infinite;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
@keyframes dc-spin { to { transform: rotate(360deg); } }
|
|
1982
|
+
|
|
1983
|
+
#dc-preview .dc-pv-warn {
|
|
1984
|
+
display: flex;
|
|
1985
|
+
flex-wrap: wrap;
|
|
1986
|
+
align-items: center;
|
|
1987
|
+
gap: 8px;
|
|
1988
|
+
padding: 10px 12px;
|
|
1989
|
+
border-radius: 10px;
|
|
1990
|
+
background: var(--dc-danger-soft);
|
|
1991
|
+
color: var(--dc-danger-ink);
|
|
1992
|
+
font-size: 13px;
|
|
1993
|
+
font-weight: 700;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
#dc-preview .dc-pv-blocked {
|
|
1997
|
+
margin: 6px 0;
|
|
1998
|
+
padding: 14px;
|
|
1999
|
+
border: 1px dashed var(--dc-border);
|
|
2000
|
+
border-radius: 8px;
|
|
2001
|
+
color: var(--dc-text-dim);
|
|
2002
|
+
font-size: 12px;
|
|
2003
|
+
text-align: center;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
#dc-preview .dc-pv-embed { display: inline-block; margin: 6px 0; font-weight: 700; }
|
|
2007
|
+
|
|
2008
|
+
.dc-pv-anchor { text-decoration: underline !important; text-decoration-color: var(--dc-accent-ink) !important; }
|
|
2009
|
+
|
|
2010
|
+
.dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
|
|
2011
|
+
|
|
1833
2012
|
#dc-custom-modal .dc-about {
|
|
1834
2013
|
margin-top: 14px;
|
|
1835
2014
|
padding: 12px 13px;
|
|
@@ -3234,7 +3413,7 @@
|
|
|
3234
3413
|
|
|
3235
3414
|
window.addEventListener('beforeunload', noteNetLeaving);
|
|
3236
3415
|
|
|
3237
|
-
|
|
3416
|
+
window.addEventListener('click', event => {
|
|
3238
3417
|
const link = event.target instanceof Element ? event.target.closest('a[href]') : null;
|
|
3239
3418
|
|
|
3240
3419
|
if (!link || event.defaultPrevented || event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
@@ -3253,7 +3432,7 @@
|
|
|
3253
3432
|
}
|
|
3254
3433
|
} catch {
|
|
3255
3434
|
}
|
|
3256
|
-
}
|
|
3435
|
+
});
|
|
3257
3436
|
|
|
3258
3437
|
try {
|
|
3259
3438
|
if (window.navigation && typeof window.navigation.addEventListener === 'function') {
|
|
@@ -5823,6 +6002,38 @@
|
|
|
5823
6002
|
// =========================================================
|
|
5824
6003
|
|
|
5825
6004
|
let toastTimer = null;
|
|
6005
|
+
const toastListeners = [];
|
|
6006
|
+
|
|
6007
|
+
const notifyToastListeners = () => toastListeners.forEach(fn => {
|
|
6008
|
+
try {
|
|
6009
|
+
fn();
|
|
6010
|
+
} catch {
|
|
6011
|
+
}
|
|
6012
|
+
});
|
|
6013
|
+
|
|
6014
|
+
const placeToast = toast => {
|
|
6015
|
+
toast.style.bottom = '';
|
|
6016
|
+
|
|
6017
|
+
const fab = document.getElementById('dc-custom-fab');
|
|
6018
|
+
|
|
6019
|
+
if (!fab || getComputedStyle(fab).display === 'none') {
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
6022
|
+
|
|
6023
|
+
const f = fab.getBoundingClientRect();
|
|
6024
|
+
const t = toast.getBoundingClientRect();
|
|
6025
|
+
|
|
6026
|
+
if (!f.width || !t.width) {
|
|
6027
|
+
return;
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
const overlapX = t.left < f.right + 6 && t.right > f.left - 6;
|
|
6031
|
+
const overlapY = t.top < f.bottom + 6 && t.bottom > f.top - 6;
|
|
6032
|
+
|
|
6033
|
+
if (overlapX && overlapY) {
|
|
6034
|
+
toast.style.bottom = `${Math.round(window.innerHeight - f.top + 10)}px`;
|
|
6035
|
+
}
|
|
6036
|
+
};
|
|
5826
6037
|
|
|
5827
6038
|
const showToast = (text, { actionLabel = '', onAction = null, duration = 3500 } = {}) => {
|
|
5828
6039
|
let toast = document.getElementById('dc-toast');
|
|
@@ -5851,8 +6062,13 @@
|
|
|
5851
6062
|
};
|
|
5852
6063
|
|
|
5853
6064
|
toast.classList.add('show');
|
|
6065
|
+
placeToast(toast);
|
|
6066
|
+
notifyToastListeners();
|
|
5854
6067
|
clearTimeout(toastTimer);
|
|
5855
|
-
toastTimer = setTimeout(() =>
|
|
6068
|
+
toastTimer = setTimeout(() => {
|
|
6069
|
+
toast.classList.remove('show');
|
|
6070
|
+
setTimeout(notifyToastListeners, 220);
|
|
6071
|
+
}, duration);
|
|
5856
6072
|
};
|
|
5857
6073
|
|
|
5858
6074
|
const offerUndo = (text, undo) => {
|
|
@@ -6402,7 +6618,97 @@
|
|
|
6402
6618
|
|
|
6403
6619
|
const currentPostKey = () => extractPostPath(location.href) || location.pathname;
|
|
6404
6620
|
|
|
6405
|
-
const
|
|
6621
|
+
const HIT_DAY_MS = 86400000;
|
|
6622
|
+
const hitToday = () => Math.floor(Date.now() / HIT_DAY_MS);
|
|
6623
|
+
|
|
6624
|
+
const readBlockHits = () => {
|
|
6625
|
+
const stored = readObject(STORAGE.blockHits);
|
|
6626
|
+
|
|
6627
|
+
return {
|
|
6628
|
+
since: Number(stored.since) || hitToday(),
|
|
6629
|
+
k: stored.k && typeof stored.k === 'object' && !Array.isArray(stored.k) ? { ...stored.k } : {}
|
|
6630
|
+
};
|
|
6631
|
+
};
|
|
6632
|
+
|
|
6633
|
+
let blockHits = readBlockHits();
|
|
6634
|
+
let blockHitsDirty = !readObject(STORAGE.blockHits).since;
|
|
6635
|
+
let blockHitsTimer = null;
|
|
6636
|
+
|
|
6637
|
+
const blockHitKeys = () => [
|
|
6638
|
+
...blockedUsers,
|
|
6639
|
+
...blockedTitles.map(word => `t:${word}`),
|
|
6640
|
+
...blockedKeywords.map(word => `w:${word}`)
|
|
6641
|
+
];
|
|
6642
|
+
|
|
6643
|
+
const flushBlockHits = () => {
|
|
6644
|
+
clearTimeout(blockHitsTimer);
|
|
6645
|
+
blockHitsTimer = null;
|
|
6646
|
+
|
|
6647
|
+
if (!blockHitsDirty) {
|
|
6648
|
+
return;
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
blockHitsDirty = false;
|
|
6652
|
+
|
|
6653
|
+
const today = hitToday();
|
|
6654
|
+
const next = {};
|
|
6655
|
+
|
|
6656
|
+
blockHitKeys().forEach(key => {
|
|
6657
|
+
next[key] = Number(blockHits.k[key]) || today;
|
|
6658
|
+
});
|
|
6659
|
+
|
|
6660
|
+
blockHits = { since: blockHits.since, k: next };
|
|
6661
|
+
writeJson(STORAGE.blockHits, blockHits);
|
|
6662
|
+
};
|
|
6663
|
+
|
|
6664
|
+
const scheduleBlockHitsFlush = () => {
|
|
6665
|
+
blockHitsDirty = true;
|
|
6666
|
+
|
|
6667
|
+
if (!blockHitsTimer) {
|
|
6668
|
+
blockHitsTimer = setTimeout(flushBlockHits, 20000);
|
|
6669
|
+
}
|
|
6670
|
+
};
|
|
6671
|
+
|
|
6672
|
+
const noteBlockHit = reason => {
|
|
6673
|
+
if (!reason) {
|
|
6674
|
+
return reason;
|
|
6675
|
+
}
|
|
6676
|
+
|
|
6677
|
+
const keys = reason.type === 'user'
|
|
6678
|
+
? reason.userKeys || []
|
|
6679
|
+
: reason.type === 'title'
|
|
6680
|
+
? [`t:${reason.word}`]
|
|
6681
|
+
: reason.type === 'keyword'
|
|
6682
|
+
? [blockedTitles.includes(reason.word) && !blockedKeywords.includes(reason.word) ? `t:${reason.word}` : `w:${reason.word}`]
|
|
6683
|
+
: [];
|
|
6684
|
+
const today = hitToday();
|
|
6685
|
+
|
|
6686
|
+
keys.forEach(key => {
|
|
6687
|
+
if (blockHits.k[key] !== today) {
|
|
6688
|
+
blockHits.k[key] = today;
|
|
6689
|
+
scheduleBlockHitsFlush();
|
|
6690
|
+
}
|
|
6691
|
+
});
|
|
6692
|
+
|
|
6693
|
+
return reason;
|
|
6694
|
+
};
|
|
6695
|
+
|
|
6696
|
+
window.addEventListener('pagehide', flushBlockHits);
|
|
6697
|
+
document.addEventListener('visibilitychange', () => {
|
|
6698
|
+
if (document.visibilityState === 'hidden') {
|
|
6699
|
+
flushBlockHits();
|
|
6700
|
+
}
|
|
6701
|
+
});
|
|
6702
|
+
|
|
6703
|
+
storageWriteListeners.push(key => {
|
|
6704
|
+
if (key === STORAGE.users || key === STORAGE.keywords || key === STORAGE.titles) {
|
|
6705
|
+
scheduleBlockHitsFlush();
|
|
6706
|
+
}
|
|
6707
|
+
});
|
|
6708
|
+
|
|
6709
|
+
const computeBlockReason = el => noteBlockHit(computeBlockReasonRaw(el));
|
|
6710
|
+
|
|
6711
|
+
const computeBlockReasonRaw = el => {
|
|
6406
6712
|
const hits = buildBlockKeys({
|
|
6407
6713
|
nickname: el.dataset.dcNick || '',
|
|
6408
6714
|
ip: el.dataset.dcIp || '',
|
|
@@ -9927,6 +10233,7 @@
|
|
|
9927
10233
|
});
|
|
9928
10234
|
|
|
9929
10235
|
area.appendChild(seg);
|
|
10236
|
+
renderCleanupHelper(area);
|
|
9930
10237
|
(renderBlockSections[blockSection] || renderBlockSections.keywords)(area);
|
|
9931
10238
|
};
|
|
9932
10239
|
|
|
@@ -10113,6 +10420,15 @@
|
|
|
10113
10420
|
off: '사이트 기본 한 줄 알림을 그대로 씁니다.',
|
|
10114
10421
|
refresh: 'root'
|
|
10115
10422
|
},
|
|
10423
|
+
{
|
|
10424
|
+
group: 'alarm',
|
|
10425
|
+
key: 'alarmSystemNotify',
|
|
10426
|
+
parent: 'alarmBubble',
|
|
10427
|
+
name: '다른 탭을 볼 때 기기 알림으로도 (PC)',
|
|
10428
|
+
on: '이 탭을 보고 있지 않을 때 디시 알림이 오면 윈도우·맥 알림으로 띄웁니다. 누르면 이 탭으로 돌아와 해당 글로 갑니다. 처음 켤 때 브라우저가 권한을 묻습니다.',
|
|
10429
|
+
off: '이 탭을 볼 때 말풍선으로만 알립니다.',
|
|
10430
|
+
refresh: 'root'
|
|
10431
|
+
},
|
|
10116
10432
|
{
|
|
10117
10433
|
group: 'alarm',
|
|
10118
10434
|
key: 'alarmKeepPolling',
|
|
@@ -10138,6 +10454,22 @@
|
|
|
10138
10454
|
off: '모든 댓글을 보여 줍니다.',
|
|
10139
10455
|
refresh: 'all'
|
|
10140
10456
|
},
|
|
10457
|
+
{
|
|
10458
|
+
group: 'list',
|
|
10459
|
+
key: 'keyboardShortcuts',
|
|
10460
|
+
name: '키보드 단축키 (PC)',
|
|
10461
|
+
on: '목록에서 J/K 로 글을 고르고 Enter 로 엽니다. 글에서는 J/K 로 다음·이전 글, S 스크랩, / 검색창, B 차단 관리, ? 안내.',
|
|
10462
|
+
off: '단축키를 쓰지 않습니다.',
|
|
10463
|
+
refresh: 'none'
|
|
10464
|
+
},
|
|
10465
|
+
{
|
|
10466
|
+
group: 'list',
|
|
10467
|
+
key: 'hoverPreview',
|
|
10468
|
+
name: '제목에 마우스를 올리면 글 미리보기 (PC)',
|
|
10469
|
+
on: '목록 제목 위에 잠깐 머무르면 글 전체를 옆 창에 보여 줍니다. 창 안에서 스크롤해 읽고, 스크랩·작성자 차단도 바로 할 수 있습니다. 차단한 이미지·단어는 미리보기에서도 가립니다. 키보드로 고른 글은 P 로 엽니다.',
|
|
10470
|
+
off: '제목을 눌러야 글을 봅니다.',
|
|
10471
|
+
refresh: 'root'
|
|
10472
|
+
},
|
|
10141
10473
|
{
|
|
10142
10474
|
group: 'list',
|
|
10143
10475
|
key: 'autoFillList',
|
|
@@ -10335,6 +10667,20 @@
|
|
|
10335
10667
|
paintAlarmBadge();
|
|
10336
10668
|
}
|
|
10337
10669
|
|
|
10670
|
+
if (item.key === 'hoverPreview' && settings.hoverPreview === false) {
|
|
10671
|
+
closePreview();
|
|
10672
|
+
}
|
|
10673
|
+
|
|
10674
|
+
if (item.key === 'alarmSystemNotify' && settings.alarmSystemNotify === true) {
|
|
10675
|
+
requestSystemNotify().then(granted => {
|
|
10676
|
+
if (!granted) {
|
|
10677
|
+
settings.alarmSystemNotify = false;
|
|
10678
|
+
saveSettings();
|
|
10679
|
+
rerenderModal();
|
|
10680
|
+
}
|
|
10681
|
+
});
|
|
10682
|
+
}
|
|
10683
|
+
|
|
10338
10684
|
break;
|
|
10339
10685
|
|
|
10340
10686
|
case 'log':
|
|
@@ -11295,7 +11641,11 @@
|
|
|
11295
11641
|
'<button id="dc-modal-close-btn" class="dc-head-btn" type="button">닫기</button>' +
|
|
11296
11642
|
'</div>' +
|
|
11297
11643
|
`<div class="dc-modal-tabs">${Object.keys(MODAL_VIEWS)
|
|
11298
|
-
.map(key =>
|
|
11644
|
+
.map(key => {
|
|
11645
|
+
const [ico, ...rest] = MODAL_VIEWS[key].tab.split(' ');
|
|
11646
|
+
|
|
11647
|
+
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>`;
|
|
11648
|
+
})
|
|
11299
11649
|
.join('')}</div>` +
|
|
11300
11650
|
'</div>' +
|
|
11301
11651
|
'<div id="dc-modal-content"></div>';
|
|
@@ -11385,6 +11735,7 @@
|
|
|
11385
11735
|
{ key: 'heads', label: '말머리', fields: ['heads'] },
|
|
11386
11736
|
{ key: 'scraps', label: '스크랩', fields: ['scraps'] },
|
|
11387
11737
|
{ key: 'recent', label: '최근 갤러리', fields: ['recent'] },
|
|
11738
|
+
{ key: 'search', label: '검색 기록', fields: ['search'] },
|
|
11388
11739
|
{ key: 'settings', label: '설정', fields: ['settings'] }
|
|
11389
11740
|
];
|
|
11390
11741
|
|
|
@@ -11552,7 +11903,9 @@
|
|
|
11552
11903
|
heads: JSON.parse(JSON.stringify(hiddenHeadMap || {})),
|
|
11553
11904
|
scraps: scraps.map(item => ({ ...item })),
|
|
11554
11905
|
settings: syncedSettings(),
|
|
11555
|
-
recent: collectRecentForSync()
|
|
11906
|
+
recent: collectRecentForSync(),
|
|
11907
|
+
search: collectSearchForSync(),
|
|
11908
|
+
hits: collectHitsForSync()
|
|
11556
11909
|
});
|
|
11557
11910
|
|
|
11558
11911
|
const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
|
|
@@ -11659,7 +12012,9 @@
|
|
|
11659
12012
|
heads: mergeHeads(local.heads, base && base.heads, remote && remote.heads),
|
|
11660
12013
|
scraps: mergeScraps(local.scraps, base && base.scraps, remote && remote.scraps),
|
|
11661
12014
|
settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings),
|
|
11662
|
-
recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent)
|
|
12015
|
+
recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
|
|
12016
|
+
search: mergeSearchHistory(local.search, base && base.search, remote && remote.search),
|
|
12017
|
+
hits: mergeBlockHits(local.hits, remote && remote.hits)
|
|
11663
12018
|
};
|
|
11664
12019
|
|
|
11665
12020
|
skippedFields(skip).forEach(field => {
|
|
@@ -11713,12 +12068,16 @@
|
|
|
11713
12068
|
const data = collectSyncData();
|
|
11714
12069
|
|
|
11715
12070
|
delete data.recent;
|
|
12071
|
+
delete data.search;
|
|
12072
|
+
delete data.hits;
|
|
11716
12073
|
|
|
11717
12074
|
return JSON.stringify(data);
|
|
11718
12075
|
};
|
|
11719
12076
|
|
|
11720
12077
|
const applySyncData = data => {
|
|
11721
12078
|
const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
|
|
12079
|
+
const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
|
|
12080
|
+
const hitsChanged = data && 'hits' in data ? applyHitsFromSync(data.hits) : false;
|
|
11722
12081
|
const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
|
|
11723
12082
|
const before = blockSyncData();
|
|
11724
12083
|
|
|
@@ -11773,7 +12132,7 @@
|
|
|
11773
12132
|
refreshAfterBlockChange();
|
|
11774
12133
|
}
|
|
11775
12134
|
|
|
11776
|
-
return changed || recentChanged;
|
|
12135
|
+
return changed || recentChanged || searchChanged || hitsChanged;
|
|
11777
12136
|
};
|
|
11778
12137
|
|
|
11779
12138
|
const saveSyncUndo = (before, incoming, summary, from) => {
|
|
@@ -13134,8 +13493,12 @@
|
|
|
13134
13493
|
const nativeKeys = new Set(native.map(recentKeyOf));
|
|
13135
13494
|
const snapshot = readArray(STORAGE.recentNative).filter(key => typeof key === 'string');
|
|
13136
13495
|
|
|
13137
|
-
if (snapshot.length
|
|
13138
|
-
const
|
|
13496
|
+
if (snapshot.length) {
|
|
13497
|
+
const snapSet = new Set(snapshot);
|
|
13498
|
+
const added = native.filter(item => !snapSet.has(recentKeyOf(item))).length;
|
|
13499
|
+
const overflow = Math.max(0, snapshot.length + added - RECENT_NATIVE_CAP);
|
|
13500
|
+
const kept = snapshot.slice(0, snapshot.length - overflow);
|
|
13501
|
+
const removed = new Set(kept.filter(key => !nativeKeys.has(key)));
|
|
13139
13502
|
|
|
13140
13503
|
if (removed.size) {
|
|
13141
13504
|
recentGalls = recentGalls.filter(item => !removed.has(recentKeyOf(item)));
|
|
@@ -13489,24 +13852,40 @@
|
|
|
13489
13852
|
}
|
|
13490
13853
|
|
|
13491
13854
|
const fab = document.getElementById('dc-custom-fab');
|
|
13492
|
-
const rect = fab && fab.
|
|
13855
|
+
const rect = fab && getComputedStyle(fab).display !== 'none' ? fab.getBoundingClientRect() : null;
|
|
13493
13856
|
const vw = window.innerWidth;
|
|
13494
13857
|
const vh = window.innerHeight;
|
|
13858
|
+
let bottom = 144;
|
|
13495
13859
|
|
|
13496
13860
|
if (rect && rect.width) {
|
|
13497
13861
|
const onRight = rect.left + rect.width / 2 > vw / 2;
|
|
13498
13862
|
|
|
13499
13863
|
bubbleBox.style.left = onRight ? '' : `${Math.max(8, rect.left)}px`;
|
|
13500
13864
|
bubbleBox.style.right = onRight ? `${Math.max(8, vw - rect.right)}px` : '';
|
|
13501
|
-
|
|
13865
|
+
bottom = Math.max(8, vh - rect.top + 10);
|
|
13502
13866
|
bubbleBox.classList.toggle('dc-left', !onRight);
|
|
13503
13867
|
} else {
|
|
13504
13868
|
bubbleBox.style.left = '';
|
|
13505
13869
|
bubbleBox.style.right = '15px';
|
|
13506
|
-
bubbleBox.
|
|
13870
|
+
bubbleBox.classList.remove('dc-left');
|
|
13871
|
+
}
|
|
13872
|
+
|
|
13873
|
+
const toast = document.getElementById('dc-toast');
|
|
13874
|
+
|
|
13875
|
+
if (toast && toast.classList.contains('show')) {
|
|
13876
|
+
const t = toast.getBoundingClientRect();
|
|
13877
|
+
const box = bubbleBox.getBoundingClientRect();
|
|
13878
|
+
|
|
13879
|
+
if (t.width && t.left < box.right && t.right > box.left) {
|
|
13880
|
+
bottom = Math.max(bottom, vh - t.top + 8);
|
|
13881
|
+
}
|
|
13507
13882
|
}
|
|
13883
|
+
|
|
13884
|
+
bubbleBox.style.bottom = `${Math.round(bottom)}px`;
|
|
13508
13885
|
};
|
|
13509
13886
|
|
|
13887
|
+
toastListeners.push(() => placeBubbleBox());
|
|
13888
|
+
|
|
13510
13889
|
const ensureBubbleBox = () => {
|
|
13511
13890
|
if (bubbleBox && bubbleBox.isConnected) {
|
|
13512
13891
|
return bubbleBox;
|
|
@@ -13654,7 +14033,9 @@
|
|
|
13654
14033
|
let alarmWatchTimer = null;
|
|
13655
14034
|
|
|
13656
14035
|
const checkSiteAlarms = () => {
|
|
13657
|
-
|
|
14036
|
+
const away = document.visibilityState === 'hidden' || (typeof document.hasFocus === 'function' && !document.hasFocus());
|
|
14037
|
+
|
|
14038
|
+
if (settings.alarmBubble === false || (document.visibilityState === 'hidden' && !canSystemNotify())) {
|
|
13658
14039
|
return;
|
|
13659
14040
|
}
|
|
13660
14041
|
|
|
@@ -13682,7 +14063,11 @@
|
|
|
13682
14063
|
writeAlarmMarks({ shown: latest });
|
|
13683
14064
|
}
|
|
13684
14065
|
|
|
13685
|
-
if (fresh.length) {
|
|
14066
|
+
if (fresh.length && away && canSystemNotify()) {
|
|
14067
|
+
showSystemAlarm(fresh[0], fresh.length - 1);
|
|
14068
|
+
}
|
|
14069
|
+
|
|
14070
|
+
if (fresh.length && document.visibilityState !== 'hidden') {
|
|
13686
14071
|
showAlarmBubble(fresh[0], fresh.length > ALARM_BUBBLE_MAX ? fresh.length - 1 : 0);
|
|
13687
14072
|
|
|
13688
14073
|
if (fresh.length > 1 && fresh.length <= ALARM_BUBBLE_MAX) {
|
|
@@ -13730,9 +14115,24 @@
|
|
|
13730
14115
|
}
|
|
13731
14116
|
};
|
|
13732
14117
|
|
|
13733
|
-
|
|
13734
|
-
if (alarmWatchTimer || !
|
|
14118
|
+
function initSiteAlarms() {
|
|
14119
|
+
if (alarmWatchTimer || !(IS_MOBILE_SITE || /^gall\.dcinside\.com$/i.test(location.hostname))) {
|
|
14120
|
+
paintAlarmBadge();
|
|
14121
|
+
return;
|
|
14122
|
+
}
|
|
14123
|
+
|
|
14124
|
+
if (!siteUserId() && !alarmStorageKeys().length) {
|
|
13735
14125
|
paintAlarmBadge();
|
|
14126
|
+
|
|
14127
|
+
if (!initSiteAlarms.retried) {
|
|
14128
|
+
initSiteAlarms.retried = true;
|
|
14129
|
+
[3000, 10000].forEach(delay => setTimeout(() => {
|
|
14130
|
+
if (!alarmWatchTimer && (siteUserId() || alarmStorageKeys().length)) {
|
|
14131
|
+
initSiteAlarms();
|
|
14132
|
+
}
|
|
14133
|
+
}, delay));
|
|
14134
|
+
}
|
|
14135
|
+
|
|
13736
14136
|
return;
|
|
13737
14137
|
}
|
|
13738
14138
|
|
|
@@ -13873,6 +14273,889 @@
|
|
|
13873
14273
|
})();
|
|
13874
14274
|
|
|
13875
14275
|
initRecentEarly();
|
|
14276
|
+
|
|
14277
|
+
// =========================================================
|
|
14278
|
+
|
|
14279
|
+
// =========================================================
|
|
14280
|
+
|
|
14281
|
+
const KEY_HELP = 'J/K 다음·이전 글 · Enter 열기 · P 미리보기 · S 스크랩 · / 검색 · B 차단 관리';
|
|
14282
|
+
|
|
14283
|
+
const typingTarget = el =>
|
|
14284
|
+
!!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || !!el.closest?.('[contenteditable="true"]'));
|
|
14285
|
+
|
|
14286
|
+
const visibleListRows = () => {
|
|
14287
|
+
const listEl = pickListElement(document);
|
|
14288
|
+
|
|
14289
|
+
if (!listEl) {
|
|
14290
|
+
return [];
|
|
14291
|
+
}
|
|
14292
|
+
|
|
14293
|
+
return realListItems(listEl).filter(row => row.getClientRects().length && listItemKey(row) && !row.closest('.dc-ui'));
|
|
14294
|
+
};
|
|
14295
|
+
|
|
14296
|
+
let keyCursor = null;
|
|
14297
|
+
|
|
14298
|
+
const moveKeyCursor = step => {
|
|
14299
|
+
const rows = visibleListRows();
|
|
14300
|
+
|
|
14301
|
+
if (!rows.length) {
|
|
14302
|
+
return false;
|
|
14303
|
+
}
|
|
14304
|
+
|
|
14305
|
+
const here = currentPostKey();
|
|
14306
|
+
let index = keyCursor ? rows.indexOf(keyCursor) : -1;
|
|
14307
|
+
|
|
14308
|
+
if (currentPostInfo()) {
|
|
14309
|
+
const at = rows.findIndex(row => listItemKey(row) === here);
|
|
14310
|
+
const target = at >= 0 ? rows[at + step] : step > 0 ? rows[0] : null;
|
|
14311
|
+
const link = target && target.querySelector(POST_LINK_SELECTOR);
|
|
14312
|
+
|
|
14313
|
+
if (link) {
|
|
14314
|
+
location.href = link.href;
|
|
14315
|
+
return true;
|
|
14316
|
+
}
|
|
14317
|
+
|
|
14318
|
+
showToast(step > 0 ? '아래에 더 볼 글이 없습니다' : '첫 글입니다');
|
|
14319
|
+
return true;
|
|
14320
|
+
}
|
|
14321
|
+
|
|
14322
|
+
if (index < 0) {
|
|
14323
|
+
index = rows.findIndex(row => row.getBoundingClientRect().bottom > 60);
|
|
14324
|
+
index = Math.max(0, step > 0 ? index : index);
|
|
14325
|
+
} else {
|
|
14326
|
+
index = Math.min(rows.length - 1, Math.max(0, index + step));
|
|
14327
|
+
}
|
|
14328
|
+
|
|
14329
|
+
keyCursor?.classList.remove('dc-key-cursor');
|
|
14330
|
+
keyCursor = rows[index];
|
|
14331
|
+
keyCursor.classList.add('dc-key-cursor');
|
|
14332
|
+
keyCursor.scrollIntoView({ block: 'nearest' });
|
|
14333
|
+
|
|
14334
|
+
return true;
|
|
14335
|
+
};
|
|
14336
|
+
|
|
14337
|
+
const initKeyboardShortcuts = () => {
|
|
14338
|
+
document.addEventListener('keydown', event => {
|
|
14339
|
+
if (settings.keyboardShortcuts === false || event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) {
|
|
14340
|
+
return;
|
|
14341
|
+
}
|
|
14342
|
+
|
|
14343
|
+
if (typingTarget(event.target) || typingTarget(document.activeElement)) {
|
|
14344
|
+
return;
|
|
14345
|
+
}
|
|
14346
|
+
|
|
14347
|
+
if (document.getElementById('dc-custom-modal')?.classList.contains('show')) {
|
|
14348
|
+
return;
|
|
14349
|
+
}
|
|
14350
|
+
|
|
14351
|
+
const key = event.key;
|
|
14352
|
+
let handled = false;
|
|
14353
|
+
|
|
14354
|
+
if (key === 'j' || key === 'J') {
|
|
14355
|
+
handled = moveKeyCursor(1);
|
|
14356
|
+
} else if (key === 'k' || key === 'K') {
|
|
14357
|
+
handled = moveKeyCursor(-1);
|
|
14358
|
+
} else if ((key === 'Enter' || key === 'o' || key === 'O') && keyCursor && keyCursor.isConnected) {
|
|
14359
|
+
const link = keyCursor.querySelector(POST_LINK_SELECTOR);
|
|
14360
|
+
|
|
14361
|
+
if (link) {
|
|
14362
|
+
location.href = link.href;
|
|
14363
|
+
handled = true;
|
|
14364
|
+
}
|
|
14365
|
+
} else if (key === 's' || key === 'S') {
|
|
14366
|
+
const info = currentPostInfo();
|
|
14367
|
+
|
|
14368
|
+
if (info) {
|
|
14369
|
+
toggleScrap(info);
|
|
14370
|
+
handled = true;
|
|
14371
|
+
}
|
|
14372
|
+
} else if (key === '/') {
|
|
14373
|
+
const box = document.querySelector('.in_keyword, #preSWord, input.js-keyword, input[name="search_keyword"], .top_search input[type="text"]');
|
|
14374
|
+
|
|
14375
|
+
if (box) {
|
|
14376
|
+
box.focus();
|
|
14377
|
+
handled = true;
|
|
14378
|
+
}
|
|
14379
|
+
} else if (key === 'b' || key === 'B') {
|
|
14380
|
+
openBlockModal(document.getElementById('dc-custom-fab')?.dataset.dcAlarm ? 'recent' : 'overview');
|
|
14381
|
+
handled = true;
|
|
14382
|
+
} else if ((key === 'p' || key === 'P') && togglePreviewForCursor()) {
|
|
14383
|
+
handled = true;
|
|
14384
|
+
} else if (key === '?') {
|
|
14385
|
+
showToast(`⌨️ ${KEY_HELP}`, { duration: 6000 });
|
|
14386
|
+
handled = true;
|
|
14387
|
+
}
|
|
14388
|
+
|
|
14389
|
+
if (handled) {
|
|
14390
|
+
event.preventDefault();
|
|
14391
|
+
}
|
|
14392
|
+
});
|
|
14393
|
+
};
|
|
14394
|
+
|
|
14395
|
+
// =========================================================
|
|
14396
|
+
|
|
14397
|
+
// =========================================================
|
|
14398
|
+
|
|
14399
|
+
const SEARCH_HISTORY_MAX = 30;
|
|
14400
|
+
|
|
14401
|
+
const readSiteSearchHistory = () => {
|
|
14402
|
+
if (!IS_MOBILE_SITE) {
|
|
14403
|
+
return null;
|
|
14404
|
+
}
|
|
14405
|
+
|
|
14406
|
+
try {
|
|
14407
|
+
const list = JSON.parse(localStorage.getItem('searchHistory') || '[]');
|
|
14408
|
+
|
|
14409
|
+
return Array.isArray(list) ? list.filter(item => typeof item === 'string' && item.trim()).map(item => item.slice(0, 60)) : [];
|
|
14410
|
+
} catch {
|
|
14411
|
+
return [];
|
|
14412
|
+
}
|
|
14413
|
+
};
|
|
14414
|
+
|
|
14415
|
+
const collectSearchForSync = () => {
|
|
14416
|
+
const site = readSiteSearchHistory();
|
|
14417
|
+
const list = site || readArray(STORAGE.searchHistory).filter(item => typeof item === 'string');
|
|
14418
|
+
|
|
14419
|
+
return [...new Set(list)].slice(-SEARCH_HISTORY_MAX);
|
|
14420
|
+
};
|
|
14421
|
+
|
|
14422
|
+
const mergeSearchHistory = (local = [], base, remote) => {
|
|
14423
|
+
if (!Array.isArray(remote)) {
|
|
14424
|
+
return [...new Set(local || [])].slice(-SEARCH_HISTORY_MAX);
|
|
14425
|
+
}
|
|
14426
|
+
|
|
14427
|
+
return mergeList(local || [], Array.isArray(base) ? base : null, remote).slice(-SEARCH_HISTORY_MAX);
|
|
14428
|
+
};
|
|
14429
|
+
|
|
14430
|
+
const applySearchFromSync = list => {
|
|
14431
|
+
if (!Array.isArray(list)) {
|
|
14432
|
+
return false;
|
|
14433
|
+
}
|
|
14434
|
+
|
|
14435
|
+
const next = [...new Set(list.filter(item => typeof item === 'string'))].slice(-SEARCH_HISTORY_MAX);
|
|
14436
|
+
|
|
14437
|
+
if (JSON.stringify(next) === JSON.stringify(collectSearchForSync())) {
|
|
14438
|
+
return false;
|
|
14439
|
+
}
|
|
14440
|
+
|
|
14441
|
+
writeJson(STORAGE.searchHistory, next);
|
|
14442
|
+
|
|
14443
|
+
if (IS_MOBILE_SITE) {
|
|
14444
|
+
try {
|
|
14445
|
+
localStorage.setItem('searchHistory', JSON.stringify(next));
|
|
14446
|
+
} catch {
|
|
14447
|
+
}
|
|
14448
|
+
}
|
|
14449
|
+
|
|
14450
|
+
return true;
|
|
14451
|
+
};
|
|
14452
|
+
|
|
14453
|
+
// =========================================================
|
|
14454
|
+
// 13-4. 차단 목록 정리 도우미 · 기기 알림 · PC 글 미리보기
|
|
14455
|
+
// =========================================================
|
|
14456
|
+
|
|
14457
|
+
const collectHitsForSync = () => {
|
|
14458
|
+
flushBlockHits();
|
|
14459
|
+
|
|
14460
|
+
return { since: blockHits.since, k: { ...blockHits.k } };
|
|
14461
|
+
};
|
|
14462
|
+
|
|
14463
|
+
const mergeBlockHits = (local, remote) => {
|
|
14464
|
+
if (!remote || typeof remote !== 'object' || !remote.k) {
|
|
14465
|
+
return local;
|
|
14466
|
+
}
|
|
14467
|
+
|
|
14468
|
+
const k = { ...(remote.k || {}) };
|
|
14469
|
+
|
|
14470
|
+
Object.entries((local && local.k) || {}).forEach(([key, day]) => {
|
|
14471
|
+
k[key] = Math.max(Number(k[key]) || 0, Number(day) || 0);
|
|
14472
|
+
});
|
|
14473
|
+
|
|
14474
|
+
return { since: Math.min(Number(local && local.since) || hitToday(), Number(remote.since) || hitToday()), k };
|
|
14475
|
+
};
|
|
14476
|
+
|
|
14477
|
+
const applyHitsFromSync = data => {
|
|
14478
|
+
if (!data || typeof data !== 'object' || !data.k) {
|
|
14479
|
+
return false;
|
|
14480
|
+
}
|
|
14481
|
+
|
|
14482
|
+
const merged = mergeBlockHits(blockHits, data);
|
|
14483
|
+
|
|
14484
|
+
if (JSON.stringify(merged) === JSON.stringify(blockHits)) {
|
|
14485
|
+
return false;
|
|
14486
|
+
}
|
|
14487
|
+
|
|
14488
|
+
blockHits = merged;
|
|
14489
|
+
blockHitsDirty = true;
|
|
14490
|
+
flushBlockHits();
|
|
14491
|
+
|
|
14492
|
+
return true;
|
|
14493
|
+
};
|
|
14494
|
+
|
|
14495
|
+
// =========================================================
|
|
14496
|
+
|
|
14497
|
+
// =========================================================
|
|
14498
|
+
|
|
14499
|
+
const CLEANUP_IDLE_DAYS = 60;
|
|
14500
|
+
|
|
14501
|
+
const cleanupSuggestions = () => {
|
|
14502
|
+
flushBlockHits();
|
|
14503
|
+
|
|
14504
|
+
const today = hitToday();
|
|
14505
|
+
const tracked = today - blockHits.since;
|
|
14506
|
+
const idle = [];
|
|
14507
|
+
|
|
14508
|
+
if (tracked >= CLEANUP_IDLE_DAYS) {
|
|
14509
|
+
blockHitKeys().forEach(key => {
|
|
14510
|
+
const day = Number(blockHits.k[key]) || today;
|
|
14511
|
+
|
|
14512
|
+
if (today - day >= CLEANUP_IDLE_DAYS) {
|
|
14513
|
+
idle.push({ key, days: today - day });
|
|
14514
|
+
}
|
|
14515
|
+
});
|
|
14516
|
+
}
|
|
14517
|
+
|
|
14518
|
+
const redundant = [];
|
|
14519
|
+
|
|
14520
|
+
blockedKeywords.forEach(word => {
|
|
14521
|
+
const cover = blockedKeywords.find(other => other !== word && other.length < word.length && word.includes(other));
|
|
14522
|
+
|
|
14523
|
+
if (cover) {
|
|
14524
|
+
redundant.push({ key: `w:${word}`, cover });
|
|
14525
|
+
}
|
|
14526
|
+
});
|
|
14527
|
+
|
|
14528
|
+
blockedTitles.forEach(word => {
|
|
14529
|
+
const cover = [...blockedKeywords, ...blockedTitles].find(other => other !== word && other.length < word.length && word.includes(other));
|
|
14530
|
+
|
|
14531
|
+
if (cover) {
|
|
14532
|
+
redundant.push({ key: `t:${word}`, cover });
|
|
14533
|
+
}
|
|
14534
|
+
});
|
|
14535
|
+
|
|
14536
|
+
return { idle: idle.sort((a, b) => b.days - a.days), redundant, tracked };
|
|
14537
|
+
};
|
|
14538
|
+
|
|
14539
|
+
const describeHitKey = key => {
|
|
14540
|
+
if (key.startsWith('w:')) {
|
|
14541
|
+
return { label: key.slice(2), kind: '단어' };
|
|
14542
|
+
}
|
|
14543
|
+
|
|
14544
|
+
if (key.startsWith('t:')) {
|
|
14545
|
+
return { label: key.slice(2), kind: '제목 단어' };
|
|
14546
|
+
}
|
|
14547
|
+
|
|
14548
|
+
const parsed = parseBlockKey(key);
|
|
14549
|
+
|
|
14550
|
+
return { label: parsed.value, kind: BLOCK_TYPE_LABELS[parsed.type] || '유저' };
|
|
14551
|
+
};
|
|
14552
|
+
|
|
14553
|
+
const removeBlockKeys = keys => {
|
|
14554
|
+
const before = { users: [...blockedUsers], keywords: [...blockedKeywords], titles: [...blockedTitles] };
|
|
14555
|
+
const drop = new Set(keys);
|
|
14556
|
+
|
|
14557
|
+
blockedUsers = blockedUsers.filter(key => !drop.has(key));
|
|
14558
|
+
blockedKeywords = blockedKeywords.filter(word => !drop.has(`w:${word}`));
|
|
14559
|
+
blockedTitles = blockedTitles.filter(word => !drop.has(`t:${word}`));
|
|
14560
|
+
saveUsers();
|
|
14561
|
+
saveKeywords();
|
|
14562
|
+
saveTitles();
|
|
14563
|
+
refreshAfterBlockChange();
|
|
14564
|
+
|
|
14565
|
+
showToast(`🧹 ${keys.length}개 정리함`, {
|
|
14566
|
+
actionLabel: '되돌리기',
|
|
14567
|
+
duration: 6000,
|
|
14568
|
+
onAction: () => {
|
|
14569
|
+
blockedUsers = before.users;
|
|
14570
|
+
blockedKeywords = before.keywords;
|
|
14571
|
+
blockedTitles = before.titles;
|
|
14572
|
+
saveUsers();
|
|
14573
|
+
saveKeywords();
|
|
14574
|
+
saveTitles();
|
|
14575
|
+
refreshAfterBlockChange();
|
|
14576
|
+
rerenderModal();
|
|
14577
|
+
}
|
|
14578
|
+
});
|
|
14579
|
+
};
|
|
14580
|
+
|
|
14581
|
+
const renderCleanupHelper = area => {
|
|
14582
|
+
const { idle, redundant, tracked } = cleanupSuggestions();
|
|
14583
|
+
const count = idle.length + redundant.length;
|
|
14584
|
+
const waitDays = Math.max(0, CLEANUP_IDLE_DAYS - tracked);
|
|
14585
|
+
|
|
14586
|
+
if (!count && !waitDays) {
|
|
14587
|
+
return;
|
|
14588
|
+
}
|
|
14589
|
+
|
|
14590
|
+
area.appendChild(makeFoldSection({
|
|
14591
|
+
id: 'cleanup',
|
|
14592
|
+
title: `🧹 정리 도우미${count ? '' : ' · 준비 중'}`,
|
|
14593
|
+
count,
|
|
14594
|
+
defaultOpen: false,
|
|
14595
|
+
render: body => {
|
|
14596
|
+
if (waitDays) {
|
|
14597
|
+
body.appendChild(makeNote(`어떤 차단이 실제로 쓰이는지 기록을 모으는 중입니다. ${waitDays}일 뒤부터 오래 안 걸린 항목을 추천합니다.`));
|
|
14598
|
+
}
|
|
14599
|
+
|
|
14600
|
+
const group = (title, items, meta) => {
|
|
14601
|
+
if (!items.length) {
|
|
14602
|
+
return;
|
|
14603
|
+
}
|
|
14604
|
+
|
|
14605
|
+
body.appendChild(makeSectionTitle(`${title} · ${items.length}개`));
|
|
14606
|
+
renderPaged(body, items, item => {
|
|
14607
|
+
const info = describeHitKey(item.key);
|
|
14608
|
+
|
|
14609
|
+
return makeRow({
|
|
14610
|
+
title: info.label,
|
|
14611
|
+
chip: info.kind,
|
|
14612
|
+
chipClass: 'scope',
|
|
14613
|
+
meta: meta(item),
|
|
14614
|
+
actions: [{ label: '해제', className: 'on', onClick: () => {
|
|
14615
|
+
removeBlockKeys([item.key]);
|
|
14616
|
+
rerenderModal();
|
|
14617
|
+
} }]
|
|
14618
|
+
});
|
|
14619
|
+
});
|
|
14620
|
+
body.appendChild(makeButton(`${title} 모두 해제`, 'wide', () => {
|
|
14621
|
+
if (confirm(`${title} ${items.length}개를 차단 목록에서 뺄까요? (바로 되돌릴 수 있습니다)`)) {
|
|
14622
|
+
removeBlockKeys(items.map(item => item.key));
|
|
14623
|
+
rerenderModal();
|
|
14624
|
+
}
|
|
14625
|
+
}));
|
|
14626
|
+
};
|
|
14627
|
+
|
|
14628
|
+
group('필요 없는 단어', redundant, item => `"${item.cover}" 가 이미 걸러 줌`);
|
|
14629
|
+
group(`${CLEANUP_IDLE_DAYS}일 넘게 안 걸린 항목`, idle, item => `${item.days}일 동안 걸린 글·댓글 없음 (모든 기기)`);
|
|
14630
|
+
}
|
|
14631
|
+
}));
|
|
14632
|
+
};
|
|
14633
|
+
|
|
14634
|
+
// =========================================================
|
|
14635
|
+
|
|
14636
|
+
// =========================================================
|
|
14637
|
+
|
|
14638
|
+
const systemNotifySupported = () => typeof window.Notification === 'function';
|
|
14639
|
+
|
|
14640
|
+
const canSystemNotify = () =>
|
|
14641
|
+
settings.alarmSystemNotify === true && systemNotifySupported() && window.Notification.permission === 'granted';
|
|
14642
|
+
|
|
14643
|
+
const showSystemAlarm = (item, extra = 0) => {
|
|
14644
|
+
try {
|
|
14645
|
+
const note = new window.Notification(`디시 알림 · ${item.tag}`, {
|
|
14646
|
+
body: extra ? `${item.text}\n외 ${extra}개` : item.text,
|
|
14647
|
+
tag: `dcx-${item.key}`,
|
|
14648
|
+
silent: false
|
|
14649
|
+
});
|
|
14650
|
+
|
|
14651
|
+
note.onclick = () => {
|
|
14652
|
+
try {
|
|
14653
|
+
window.focus();
|
|
14654
|
+
} catch {
|
|
14655
|
+
}
|
|
14656
|
+
|
|
14657
|
+
note.close();
|
|
14658
|
+
|
|
14659
|
+
if (extra) {
|
|
14660
|
+
openBlockModal('recent');
|
|
14661
|
+
} else {
|
|
14662
|
+
openAlarm(item);
|
|
14663
|
+
}
|
|
14664
|
+
};
|
|
14665
|
+
} catch {
|
|
14666
|
+
}
|
|
14667
|
+
};
|
|
14668
|
+
|
|
14669
|
+
const requestSystemNotify = async () => {
|
|
14670
|
+
if (!systemNotifySupported()) {
|
|
14671
|
+
showToast('이 브라우저는 기기 알림을 지원하지 않습니다');
|
|
14672
|
+
return false;
|
|
14673
|
+
}
|
|
14674
|
+
|
|
14675
|
+
let permission = window.Notification.permission;
|
|
14676
|
+
|
|
14677
|
+
if (permission === 'default') {
|
|
14678
|
+
try {
|
|
14679
|
+
permission = await window.Notification.requestPermission();
|
|
14680
|
+
} catch {
|
|
14681
|
+
permission = 'denied';
|
|
14682
|
+
}
|
|
14683
|
+
}
|
|
14684
|
+
|
|
14685
|
+
if (permission !== 'granted') {
|
|
14686
|
+
showToast('🔕 브라우저에서 이 사이트의 알림을 허용해야 기기 알림을 띄울 수 있습니다', { duration: 6000 });
|
|
14687
|
+
return false;
|
|
14688
|
+
}
|
|
14689
|
+
|
|
14690
|
+
showToast('🔔 다른 탭을 보고 있어도 디시 알림을 기기 알림으로 띄웁니다');
|
|
14691
|
+
return true;
|
|
14692
|
+
};
|
|
14693
|
+
|
|
14694
|
+
// =========================================================
|
|
14695
|
+
|
|
14696
|
+
// =========================================================
|
|
14697
|
+
|
|
14698
|
+
const PREVIEW_DELAY = 450;
|
|
14699
|
+
const PREVIEW_CACHE_MS = 5 * 60 * 1000;
|
|
14700
|
+
const PREVIEW_CACHE_MAX = 30;
|
|
14701
|
+
const PREVIEW_TITLE_SELECTOR = 'table.gall_list td.gall_tit > a:not(.reply_numbox)';
|
|
14702
|
+
|
|
14703
|
+
const previewCache = new Map();
|
|
14704
|
+
let previewEl = null;
|
|
14705
|
+
let previewKey = '';
|
|
14706
|
+
let previewAnchor = null;
|
|
14707
|
+
let previewHoverTimer = null;
|
|
14708
|
+
let previewCloseTimer = null;
|
|
14709
|
+
|
|
14710
|
+
const previewEnabled = () => IS_PC_SITE && settings.hoverPreview !== false;
|
|
14711
|
+
|
|
14712
|
+
const ensurePreviewEl = () => {
|
|
14713
|
+
if (previewEl && previewEl.isConnected) {
|
|
14714
|
+
return previewEl;
|
|
14715
|
+
}
|
|
14716
|
+
|
|
14717
|
+
previewEl = h('div', 'dc-ui');
|
|
14718
|
+
previewEl.id = 'dc-preview';
|
|
14719
|
+
previewEl.setAttribute('role', 'dialog');
|
|
14720
|
+
previewEl.setAttribute('aria-label', '글 미리보기');
|
|
14721
|
+
previewEl.innerHTML =
|
|
14722
|
+
'<div class="dc-pv-head">' +
|
|
14723
|
+
'<div class="dc-pv-title"></div>' +
|
|
14724
|
+
'<div class="dc-pv-meta"></div>' +
|
|
14725
|
+
'<div class="dc-pv-actions"></div>' +
|
|
14726
|
+
'</div>' +
|
|
14727
|
+
'<div class="dc-pv-body"></div>';
|
|
14728
|
+
previewEl.addEventListener('mouseenter', () => clearTimeout(previewCloseTimer));
|
|
14729
|
+
previewEl.addEventListener('mouseleave', () => schedulePreviewClose());
|
|
14730
|
+
document.body.appendChild(previewEl);
|
|
14731
|
+
|
|
14732
|
+
return previewEl;
|
|
14733
|
+
};
|
|
14734
|
+
|
|
14735
|
+
const placePreview = anchor => {
|
|
14736
|
+
const el = ensurePreviewEl();
|
|
14737
|
+
const rect = anchor.getBoundingClientRect();
|
|
14738
|
+
const vw = window.innerWidth;
|
|
14739
|
+
const vh = window.innerHeight;
|
|
14740
|
+
const width = Math.min(640, Math.max(360, Math.round(vw * 0.42)));
|
|
14741
|
+
const height = Math.min(Math.round(vh * 0.78), 860);
|
|
14742
|
+
|
|
14743
|
+
el.style.width = `${width}px`;
|
|
14744
|
+
el.style.maxHeight = `${height}px`;
|
|
14745
|
+
|
|
14746
|
+
let left = rect.right + 16;
|
|
14747
|
+
|
|
14748
|
+
if (left + width > vw - 12) {
|
|
14749
|
+
left = Math.max(12, rect.left - width - 16);
|
|
14750
|
+
}
|
|
14751
|
+
|
|
14752
|
+
if (left + width > vw - 12) {
|
|
14753
|
+
left = Math.max(12, vw - width - 12);
|
|
14754
|
+
}
|
|
14755
|
+
|
|
14756
|
+
const top = Math.min(Math.max(12, rect.top - 60), Math.max(12, vh - height - 12));
|
|
14757
|
+
|
|
14758
|
+
el.style.left = `${Math.round(left)}px`;
|
|
14759
|
+
el.style.top = `${Math.round(top)}px`;
|
|
14760
|
+
};
|
|
14761
|
+
|
|
14762
|
+
const closePreview = () => {
|
|
14763
|
+
clearTimeout(previewHoverTimer);
|
|
14764
|
+
clearTimeout(previewCloseTimer);
|
|
14765
|
+
|
|
14766
|
+
if (previewEl) {
|
|
14767
|
+
previewEl.classList.remove('show');
|
|
14768
|
+
previewEl.querySelector('.dc-pv-body').textContent = '';
|
|
14769
|
+
}
|
|
14770
|
+
|
|
14771
|
+
previewAnchor?.classList.remove('dc-pv-anchor');
|
|
14772
|
+
previewAnchor = null;
|
|
14773
|
+
previewKey = '';
|
|
14774
|
+
};
|
|
14775
|
+
|
|
14776
|
+
const schedulePreviewClose = () => {
|
|
14777
|
+
clearTimeout(previewCloseTimer);
|
|
14778
|
+
previewCloseTimer = setTimeout(closePreview, 260);
|
|
14779
|
+
};
|
|
14780
|
+
|
|
14781
|
+
let previewHref = '';
|
|
14782
|
+
|
|
14783
|
+
const withPostReferrer = (href, fn) => {
|
|
14784
|
+
const back = location.href;
|
|
14785
|
+
let swapped = false;
|
|
14786
|
+
|
|
14787
|
+
try {
|
|
14788
|
+
const target = new URL(href, location.href);
|
|
14789
|
+
|
|
14790
|
+
if (target.origin === location.origin && target.href !== back) {
|
|
14791
|
+
history.replaceState(history.state, '', target.pathname + target.search);
|
|
14792
|
+
swapped = true;
|
|
14793
|
+
}
|
|
14794
|
+
} catch {
|
|
14795
|
+
}
|
|
14796
|
+
|
|
14797
|
+
try {
|
|
14798
|
+
return fn();
|
|
14799
|
+
} finally {
|
|
14800
|
+
if (swapped) {
|
|
14801
|
+
setTimeout(() => {
|
|
14802
|
+
try {
|
|
14803
|
+
history.replaceState(history.state, '', back);
|
|
14804
|
+
} catch {
|
|
14805
|
+
}
|
|
14806
|
+
}, 60);
|
|
14807
|
+
}
|
|
14808
|
+
}
|
|
14809
|
+
};
|
|
14810
|
+
|
|
14811
|
+
const cleanPreviewBody = source => withPostReferrer(previewHref, () => cleanPreviewBodyRaw(source));
|
|
14812
|
+
|
|
14813
|
+
const cleanPreviewBodyRaw = source => {
|
|
14814
|
+
const body = document.importNode(source, true);
|
|
14815
|
+
let hiddenMedia = 0;
|
|
14816
|
+
|
|
14817
|
+
body.querySelectorAll('script, style, link, iframe, object, embed, form, noscript, .dc-ui').forEach(node => {
|
|
14818
|
+
if (node.tagName === 'IFRAME') {
|
|
14819
|
+
const src = node.getAttribute('src') || '';
|
|
14820
|
+
const link = document.createElement('a');
|
|
14821
|
+
|
|
14822
|
+
link.href = src;
|
|
14823
|
+
link.textContent = '🎬 삽입된 동영상 열기';
|
|
14824
|
+
link.className = 'dc-pv-embed';
|
|
14825
|
+
node.replaceWith(link);
|
|
14826
|
+
} else {
|
|
14827
|
+
node.remove();
|
|
14828
|
+
}
|
|
14829
|
+
});
|
|
14830
|
+
|
|
14831
|
+
body.querySelectorAll('*').forEach(node => {
|
|
14832
|
+
[...node.attributes].forEach(attr => {
|
|
14833
|
+
if (/^on/i.test(attr.name) || (/^(href|src)$/i.test(attr.name) && /^\s*javascript:/i.test(attr.value))) {
|
|
14834
|
+
node.removeAttribute(attr.name);
|
|
14835
|
+
}
|
|
14836
|
+
});
|
|
14837
|
+
});
|
|
14838
|
+
|
|
14839
|
+
body.querySelectorAll('img, video').forEach(media => {
|
|
14840
|
+
const lazy = media.getAttribute('data-original') || media.getAttribute('data-src');
|
|
14841
|
+
|
|
14842
|
+
if (lazy && !media.getAttribute('src')) {
|
|
14843
|
+
media.setAttribute('src', lazy);
|
|
14844
|
+
}
|
|
14845
|
+
|
|
14846
|
+
const candidates = collectMediaCandidates(media);
|
|
14847
|
+
|
|
14848
|
+
if (isDcconElement(media, candidates)) {
|
|
14849
|
+
if (settings.blockDccon !== false) {
|
|
14850
|
+
media.remove();
|
|
14851
|
+
}
|
|
14852
|
+
|
|
14853
|
+
return;
|
|
14854
|
+
}
|
|
14855
|
+
|
|
14856
|
+
const blocked = findBlockedMediaKey(resolveMediaIds(media, candidates));
|
|
14857
|
+
|
|
14858
|
+
if (blocked) {
|
|
14859
|
+
const stub = document.createElement('div');
|
|
14860
|
+
|
|
14861
|
+
stub.className = 'dc-pv-blocked';
|
|
14862
|
+
stub.textContent = '🚫 차단한 이미지';
|
|
14863
|
+
(media.closest('a') || media).replaceWith(stub);
|
|
14864
|
+
hiddenMedia += 1;
|
|
14865
|
+
return;
|
|
14866
|
+
}
|
|
14867
|
+
|
|
14868
|
+
media.removeAttribute('loading');
|
|
14869
|
+
media.removeAttribute('width');
|
|
14870
|
+
media.removeAttribute('height');
|
|
14871
|
+
|
|
14872
|
+
if (media.tagName === 'VIDEO') {
|
|
14873
|
+
media.setAttribute('controls', '');
|
|
14874
|
+
media.setAttribute('preload', 'metadata');
|
|
14875
|
+
media.removeAttribute('autoplay');
|
|
14876
|
+
}
|
|
14877
|
+
});
|
|
14878
|
+
|
|
14879
|
+
body.querySelectorAll('a[href]').forEach(link => {
|
|
14880
|
+
link.target = '_blank';
|
|
14881
|
+
link.rel = 'noopener noreferrer';
|
|
14882
|
+
});
|
|
14883
|
+
|
|
14884
|
+
return { body, hiddenMedia };
|
|
14885
|
+
};
|
|
14886
|
+
|
|
14887
|
+
const readPreviewDoc = (html, key) => {
|
|
14888
|
+
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
14889
|
+
const source = doc.querySelector('.write_div') || doc.querySelector(POST_BODY_SELECTOR);
|
|
14890
|
+
const head = doc.querySelector('.gallview_head');
|
|
14891
|
+
|
|
14892
|
+
if (!source) {
|
|
14893
|
+
return { missing: true };
|
|
14894
|
+
}
|
|
14895
|
+
|
|
14896
|
+
const writer = head?.querySelector('.gall_writer');
|
|
14897
|
+
const text = normalizeWord(`${head?.querySelector('.title_subject')?.textContent || ''} ${source.textContent}`).slice(0, BODY_TEXT_LIMIT);
|
|
14898
|
+
const word = findWord(blockedKeywords, text);
|
|
14899
|
+
|
|
14900
|
+
if (key) {
|
|
14901
|
+
bodyScanCache[key] = { s: bodyScanSignature(), w: word, m: '', t: Date.now() };
|
|
14902
|
+
saveBodyScanSoon();
|
|
14903
|
+
}
|
|
14904
|
+
|
|
14905
|
+
return {
|
|
14906
|
+
title: cleanText(head?.querySelector('.title_subject')?.textContent || ''),
|
|
14907
|
+
head: cleanText(head?.querySelector('.title_headtext')?.textContent || '').replace(/^\[|\]$/g, ''),
|
|
14908
|
+
nick: writer?.getAttribute('data-nick') || cleanText(writer?.querySelector('.nickname')?.textContent || ''),
|
|
14909
|
+
ip: writer?.getAttribute('data-ip') || '',
|
|
14910
|
+
code: writer?.getAttribute('data-uid') || '',
|
|
14911
|
+
date: head?.querySelector('.gall_date')?.getAttribute('title') || cleanText(head?.querySelector('.gall_date')?.textContent || ''),
|
|
14912
|
+
stats: (cleanText(head?.querySelector('.fr')?.textContent || '').match(/(조회|추천|댓글)\s*[\d,]+/g) || []).join(' · '),
|
|
14913
|
+
source,
|
|
14914
|
+
word
|
|
14915
|
+
};
|
|
14916
|
+
};
|
|
14917
|
+
|
|
14918
|
+
const renderPreview = (data, href, key) => {
|
|
14919
|
+
const el = ensurePreviewEl();
|
|
14920
|
+
|
|
14921
|
+
previewHref = href;
|
|
14922
|
+
const titleEl = el.querySelector('.dc-pv-title');
|
|
14923
|
+
const metaEl = el.querySelector('.dc-pv-meta');
|
|
14924
|
+
const actions = el.querySelector('.dc-pv-actions');
|
|
14925
|
+
const bodyEl = el.querySelector('.dc-pv-body');
|
|
14926
|
+
|
|
14927
|
+
actions.textContent = '';
|
|
14928
|
+
bodyEl.textContent = '';
|
|
14929
|
+
bodyEl.scrollTop = 0;
|
|
14930
|
+
el.scrollTop = 0;
|
|
14931
|
+
|
|
14932
|
+
if (data.missing) {
|
|
14933
|
+
titleEl.textContent = '본문을 불러올 수 없습니다';
|
|
14934
|
+
metaEl.textContent = '삭제됐거나 성인 인증이 필요한 글입니다.';
|
|
14935
|
+
} else {
|
|
14936
|
+
titleEl.textContent = `${data.head ? `[${data.head}] ` : ''}${data.title || '(제목 없음)'}`;
|
|
14937
|
+
metaEl.textContent = [data.nick && `${data.nick}${data.ip ? `(${data.ip})` : ''}`, data.date, data.stats].filter(Boolean).join(' · ');
|
|
14938
|
+
|
|
14939
|
+
if (data.word) {
|
|
14940
|
+
const note = h('div', 'dc-pv-warn', `🚫 본문에 차단 단어 "${data.word}" 가 있어 가렸습니다${settings.prefetchBodies !== false ? ' · 목록에서도 가려집니다' : ''}`);
|
|
14941
|
+
const reveal = makeButton('그래도 보기', 'mini', () => {
|
|
14942
|
+
note.remove();
|
|
14943
|
+
bodyEl.appendChild(cleanPreviewBody(data.source).body);
|
|
14944
|
+
});
|
|
14945
|
+
|
|
14946
|
+
note.appendChild(reveal);
|
|
14947
|
+
bodyEl.appendChild(note);
|
|
14948
|
+
} else {
|
|
14949
|
+
const { body, hiddenMedia } = cleanPreviewBody(data.source);
|
|
14950
|
+
|
|
14951
|
+
if (hiddenMedia) {
|
|
14952
|
+
bodyEl.appendChild(h('div', 'dc-pv-note', `차단한 이미지 ${hiddenMedia}개를 뺐습니다`));
|
|
14953
|
+
}
|
|
14954
|
+
|
|
14955
|
+
bodyEl.appendChild(body);
|
|
14956
|
+
}
|
|
14957
|
+
}
|
|
14958
|
+
|
|
14959
|
+
const open = h('a', 'dc-btn mini', '글 열기');
|
|
14960
|
+
|
|
14961
|
+
open.href = href;
|
|
14962
|
+
actions.appendChild(open);
|
|
14963
|
+
|
|
14964
|
+
if (!data.missing && key) {
|
|
14965
|
+
const match = /[?&]id=([^&]+)&no=(\d+)/.exec(href) || [];
|
|
14966
|
+
const kind = /\/mgallery\//.test(href) ? 'M' : /\/mini\//.test(href) ? 'N' : /\/person\//.test(href) ? 'P' : 'G';
|
|
14967
|
+
const info = match[1] ? normalizeScrap({ k: kind, id: decodeURIComponent(match[1]), no: match[2], t: data.title, g: '', d: Date.now() }) : null;
|
|
14968
|
+
|
|
14969
|
+
if (info && isValidScrap(info)) {
|
|
14970
|
+
const scrapBtn = makeButton(isScrapped(info) ? '📌 스크랩됨' : '📌 스크랩', 'mini', () => {
|
|
14971
|
+
toggleScrap(info);
|
|
14972
|
+
scrapBtn.textContent = isScrapped(info) ? '📌 스크랩됨' : '📌 스크랩';
|
|
14973
|
+
});
|
|
14974
|
+
|
|
14975
|
+
actions.appendChild(scrapBtn);
|
|
14976
|
+
}
|
|
14977
|
+
|
|
14978
|
+
const userKey = data.code ? `id::${data.code}` : data.ip ? `ip::${data.ip}` : data.nick ? `nick::${data.nick}` : '';
|
|
14979
|
+
|
|
14980
|
+
if (userKey && !blockedUsers.includes(userKey)) {
|
|
14981
|
+
actions.appendChild(makeButton('🚫 작성자 차단', 'mini danger-ghost', () => {
|
|
14982
|
+
blockedUsers = sortUserKeys([...blockedUsers, userKey]);
|
|
14983
|
+
saveUsers();
|
|
14984
|
+
refreshAfterBlockChange();
|
|
14985
|
+
closePreview();
|
|
14986
|
+
offerUndo(`✓ ${describeHitKey(userKey).kind} ${describeHitKey(userKey).label} 차단`, () => {
|
|
14987
|
+
blockedUsers = blockedUsers.filter(item => item !== userKey);
|
|
14988
|
+
saveUsers();
|
|
14989
|
+
refreshAfterBlockChange();
|
|
14990
|
+
});
|
|
14991
|
+
}));
|
|
14992
|
+
}
|
|
14993
|
+
}
|
|
14994
|
+
|
|
14995
|
+
const close = makeButton('✕', 'mini dc-pv-close', closePreview);
|
|
14996
|
+
|
|
14997
|
+
close.setAttribute('aria-label', '미리보기 닫기');
|
|
14998
|
+
actions.appendChild(close);
|
|
14999
|
+
};
|
|
15000
|
+
|
|
15001
|
+
const showPreviewFor = async anchor => {
|
|
15002
|
+
if (!previewEnabled() || !anchor || !anchor.isConnected) {
|
|
15003
|
+
return;
|
|
15004
|
+
}
|
|
15005
|
+
|
|
15006
|
+
const row = anchor.closest('tr');
|
|
15007
|
+
|
|
15008
|
+
if (row && (row.classList.contains('dc-user-blocked') && !row.classList.contains('dc-user-revealed') || row.classList.contains('dc-head-hidden'))) {
|
|
15009
|
+
return;
|
|
15010
|
+
}
|
|
15011
|
+
|
|
15012
|
+
const href = anchor.href;
|
|
15013
|
+
const key = extractPostPath(href);
|
|
15014
|
+
|
|
15015
|
+
if (!key) {
|
|
15016
|
+
return;
|
|
15017
|
+
}
|
|
15018
|
+
|
|
15019
|
+
previewAnchor?.classList.remove('dc-pv-anchor');
|
|
15020
|
+
previewAnchor = anchor;
|
|
15021
|
+
previewKey = key;
|
|
15022
|
+
anchor.classList.add('dc-pv-anchor');
|
|
15023
|
+
|
|
15024
|
+
const el = ensurePreviewEl();
|
|
15025
|
+
|
|
15026
|
+
placePreview(anchor);
|
|
15027
|
+
el.classList.add('show');
|
|
15028
|
+
|
|
15029
|
+
const cached = previewCache.get(key);
|
|
15030
|
+
|
|
15031
|
+
if (cached && Date.now() - cached.at < PREVIEW_CACHE_MS) {
|
|
15032
|
+
renderPreview(cached.data, href, key);
|
|
15033
|
+
return;
|
|
15034
|
+
}
|
|
15035
|
+
|
|
15036
|
+
const paused = netPausedUntil();
|
|
15037
|
+
|
|
15038
|
+
el.querySelector('.dc-pv-title').textContent = cleanText(anchor.textContent);
|
|
15039
|
+
el.querySelector('.dc-pv-meta').textContent = '';
|
|
15040
|
+
el.querySelector('.dc-pv-actions').textContent = '';
|
|
15041
|
+
el.querySelector('.dc-pv-body').textContent = '';
|
|
15042
|
+
|
|
15043
|
+
if (paused) {
|
|
15044
|
+
el.querySelector('.dc-pv-body').appendChild(h('div', 'dc-pv-note', `⏸ 디시 요청 제한 중이라 ${Math.max(1, Math.ceil((paused - Date.now()) / 60000))}분 뒤에 미리볼 수 있습니다`));
|
|
15045
|
+
return;
|
|
15046
|
+
}
|
|
15047
|
+
|
|
15048
|
+
el.querySelector('.dc-pv-body').appendChild(h('div', 'dc-pv-loading', '불러오는 중…'));
|
|
15049
|
+
|
|
15050
|
+
try {
|
|
15051
|
+
const html = await siteFetchText(href, { credentials: 'same-origin', cache: 'no-cache' });
|
|
15052
|
+
const data = readPreviewDoc(html, key);
|
|
15053
|
+
|
|
15054
|
+
previewCache.set(key, { at: Date.now(), data });
|
|
15055
|
+
|
|
15056
|
+
while (previewCache.size > PREVIEW_CACHE_MAX) {
|
|
15057
|
+
previewCache.delete(previewCache.keys().next().value);
|
|
15058
|
+
}
|
|
15059
|
+
|
|
15060
|
+
if (previewKey === key) {
|
|
15061
|
+
renderPreview(data, href, key);
|
|
15062
|
+
|
|
15063
|
+
if (data.word && settings.prefetchBodies !== false) {
|
|
15064
|
+
refreshAfterBlockChange();
|
|
15065
|
+
}
|
|
15066
|
+
}
|
|
15067
|
+
} catch (error) {
|
|
15068
|
+
if (previewKey !== key) {
|
|
15069
|
+
return;
|
|
15070
|
+
}
|
|
15071
|
+
|
|
15072
|
+
const body = el.querySelector('.dc-pv-body');
|
|
15073
|
+
|
|
15074
|
+
body.textContent = '';
|
|
15075
|
+
body.appendChild(h('div', 'dc-pv-note', error instanceof NetPausedError
|
|
15076
|
+
? '⏸ 디시 요청 제한 중이라 잠시 뒤에 미리볼 수 있습니다'
|
|
15077
|
+
: '불러오지 못했습니다. 잠시 뒤 다시 올려 보세요.'));
|
|
15078
|
+
}
|
|
15079
|
+
};
|
|
15080
|
+
|
|
15081
|
+
const initHoverPreview = () => {
|
|
15082
|
+
if (!IS_PC_SITE) {
|
|
15083
|
+
return;
|
|
15084
|
+
}
|
|
15085
|
+
|
|
15086
|
+
document.addEventListener('mouseover', event => {
|
|
15087
|
+
if (!previewEnabled() || (event.pointerType && event.pointerType !== 'mouse')) {
|
|
15088
|
+
return;
|
|
15089
|
+
}
|
|
15090
|
+
|
|
15091
|
+
const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
|
|
15092
|
+
|
|
15093
|
+
if (!anchor || anchor.closest('.dc-ui')) {
|
|
15094
|
+
return;
|
|
15095
|
+
}
|
|
15096
|
+
|
|
15097
|
+
clearTimeout(previewCloseTimer);
|
|
15098
|
+
|
|
15099
|
+
if (anchor === previewAnchor) {
|
|
15100
|
+
return;
|
|
15101
|
+
}
|
|
15102
|
+
|
|
15103
|
+
clearTimeout(previewHoverTimer);
|
|
15104
|
+
previewHoverTimer = setTimeout(() => showPreviewFor(anchor), PREVIEW_DELAY);
|
|
15105
|
+
});
|
|
15106
|
+
|
|
15107
|
+
document.addEventListener('mouseout', event => {
|
|
15108
|
+
const anchor = event.target instanceof Element ? event.target.closest(PREVIEW_TITLE_SELECTOR) : null;
|
|
15109
|
+
|
|
15110
|
+
if (!anchor || (event.relatedTarget instanceof Node && anchor.contains(event.relatedTarget))) {
|
|
15111
|
+
return;
|
|
15112
|
+
}
|
|
15113
|
+
|
|
15114
|
+
clearTimeout(previewHoverTimer);
|
|
15115
|
+
|
|
15116
|
+
if (previewEl && previewEl.classList.contains('show')) {
|
|
15117
|
+
schedulePreviewClose();
|
|
15118
|
+
}
|
|
15119
|
+
});
|
|
15120
|
+
|
|
15121
|
+
document.addEventListener('mousedown', event => {
|
|
15122
|
+
if (previewEl && previewEl.classList.contains('show') && !(event.target instanceof Node && previewEl.contains(event.target))) {
|
|
15123
|
+
closePreview();
|
|
15124
|
+
}
|
|
15125
|
+
}, true);
|
|
15126
|
+
|
|
15127
|
+
window.addEventListener('scroll', () => {
|
|
15128
|
+
if (previewEl && previewEl.classList.contains('show') && previewAnchor && !previewEl.matches(':hover')) {
|
|
15129
|
+
closePreview();
|
|
15130
|
+
}
|
|
15131
|
+
}, { passive: true });
|
|
15132
|
+
|
|
15133
|
+
document.addEventListener('keydown', event => {
|
|
15134
|
+
if (event.key === 'Escape' && previewEl && previewEl.classList.contains('show')) {
|
|
15135
|
+
closePreview();
|
|
15136
|
+
}
|
|
15137
|
+
});
|
|
15138
|
+
};
|
|
15139
|
+
|
|
15140
|
+
const togglePreviewForCursor = () => {
|
|
15141
|
+
if (!previewEnabled()) {
|
|
15142
|
+
return false;
|
|
15143
|
+
}
|
|
15144
|
+
|
|
15145
|
+
if (previewEl && previewEl.classList.contains('show')) {
|
|
15146
|
+
closePreview();
|
|
15147
|
+
return true;
|
|
15148
|
+
}
|
|
15149
|
+
|
|
15150
|
+
const anchor = keyCursor && keyCursor.isConnected ? keyCursor.querySelector(PREVIEW_TITLE_SELECTOR.replace('table.gall_list ', '')) : null;
|
|
15151
|
+
|
|
15152
|
+
if (!anchor) {
|
|
15153
|
+
return false;
|
|
15154
|
+
}
|
|
15155
|
+
|
|
15156
|
+
showPreviewFor(anchor);
|
|
15157
|
+
return true;
|
|
15158
|
+
};
|
|
13876
15159
|
// =========================================================
|
|
13877
15160
|
// 14. 플로팅 버튼
|
|
13878
15161
|
// =========================================================
|
|
@@ -14860,6 +16143,8 @@
|
|
|
14860
16143
|
initSync();
|
|
14861
16144
|
initRecent();
|
|
14862
16145
|
initSiteAlarms();
|
|
16146
|
+
initKeyboardShortcuts();
|
|
16147
|
+
initHoverPreview();
|
|
14863
16148
|
restoreListSnapshot();
|
|
14864
16149
|
processElements();
|
|
14865
16150
|
|