@kangsicj/dcinsideenhancer 1.1.1 โ†’ 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 CHANGED
@@ -53,6 +53,7 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
53
53
  - Hide **post tags**, notices, polls, ads and sticker-only comments
54
54
  - Catches posts whose **body** contains a blocked word, right in the list
55
55
  - Blocked by mistake? **Undo** straight from the notification
56
+ - A **cleanup helper** suggests blocks that never trigger and overlapping words
56
57
 
57
58
  </td>
58
59
  <td width="50%" valign="top">
@@ -62,7 +63,8 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
62
63
  - Gaps left by blocked posts are **refilled** from the next page
63
64
  - Come back from a post and you're **right where you left off**
64
65
  - Cleaner read-post colors and a back-to-top button
65
- - Keyboard shortcuts on PC: **J/K** next/previous post, Enter to open, S to scrap, / to search
66
+ - Keyboard shortcuts on PC: **J/K** next/previous post, Enter to open, P to preview, S to scrap, / to search
67
+ - On PC, hover a title to **preview the whole post** โ€” blocked images and words stay hidden there too
66
68
 
67
69
  </td>
68
70
  </tr>
@@ -101,6 +103,7 @@ One userscript for DCInside on mobile and PC: smart blocking, endless lists, ima
101
103
  #### ๐Ÿ”” Alerts ยท ๐Ÿ“Œ Scraps ยท ๐ŸŒ“ Theme
102
104
  - Comment and reply alerts appear as **bubbles** by the button โ€” tap to open, swipe to dismiss
103
105
  - Unread count on the button; past alerts and recent galleries in one view
106
+ - On PC, optional **desktop notifications** while you're on another tab
104
107
  - Unlimited scraps, right in the floating menu
105
108
  - Follows your device's dark mode automatically
106
109
  - Every feature can be switched on or off
package/README.md CHANGED
@@ -53,6 +53,7 @@
53
53
  - **๋ง๋จธ๋ฆฌ**, ๊ณต์ง€ยท์„ค๋ฌธยทAD, ๋””์‹œ์ฝ˜๋งŒ ์žˆ๋Š” ๋Œ“๊ธ€ ์ˆจ๊น€
54
54
  - ๋ณธ๋ฌธ์—๋งŒ ๊ธˆ์ง€์–ด๊ฐ€ ์žˆ๋Š” ๊ธ€๋„ **๋ชฉ๋ก์—์„œ ๋ฏธ๋ฆฌ** ๊ฑธ๋Ÿฌ ๋ƒ„
55
55
  - ์‹ค์ˆ˜๋กœ ๋ง‰์•˜๋‹ค๋ฉด ์•Œ๋ฆผ์—์„œ ๋ฐ”๋กœ **์‹คํ–‰ ์ทจ์†Œ**
56
+ - **์ •๋ฆฌ ๋„์šฐ๋ฏธ**๊ฐ€ ์˜ค๋ž˜ ์•ˆ ์“ฐ์ธ ์ฐจ๋‹จยท๊ฒน์น˜๋Š” ๋‹จ์–ด๋ฅผ ๊ณจ๋ผ ์คŒ
56
57
 
57
58
  </td>
58
59
  <td width="50%" valign="top">
@@ -62,7 +63,8 @@
62
63
  - ์ฐจ๋‹จ์œผ๋กœ ๋นˆ ์ž๋ฆฌ๋Š” ๋‹ค์Œ ํŽ˜์ด์ง€ ๊ธ€๋กœ **์ฑ„์›Œ ๋‘ **
63
64
  - ๊ธ€์„ ๋ณด๊ณ  ๋Œ์•„์˜ค๋ฉด **๋ณด๋˜ ์œ„์น˜ ๊ทธ๋Œ€๋กœ**
64
65
  - ์ฝ์€ ๊ธ€ ์ƒ‰ ์ •๋ฆฌ, ๋งจ ์œ„๋กœ ๋ฒ„ํŠผ
65
- - PC ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค: **J/K** ๋‹ค์Œยท์ด์ „ ๊ธ€, Enter ์—ด๊ธฐ, S ์Šคํฌ๋žฉ, / ๊ฒ€์ƒ‰
66
+ - PC ํ‚ค๋ณด๋“œ ๋‹จ์ถ•ํ‚ค: **J/K** ๋‹ค์Œยท์ด์ „ ๊ธ€, Enter ์—ด๊ธฐ, P ๋ฏธ๋ฆฌ๋ณด๊ธฐ, S ์Šคํฌ๋žฉ, / ๊ฒ€์ƒ‰
67
+ - PC๋Š” ์ œ๋ชฉ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด **๊ธ€ ์ „์ฒด ๋ฏธ๋ฆฌ๋ณด๊ธฐ** โ€” ์ฐจ๋‹จ ์ด๋ฏธ์ง€ยท๋‹จ์–ด๋Š” ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ๋„ ๊ฐ€๋ฆผ
66
68
 
67
69
  </td>
68
70
  </tr>
@@ -101,6 +103,7 @@
101
103
  #### ๐Ÿ”” ์•Œ๋ฆผ ยท ๐Ÿ“Œ ์Šคํฌ๋žฉ ยท ๐ŸŒ“ ํ…Œ๋งˆ
102
104
  - ๋Œ“๊ธ€ยท๋‹ต๊ธ€ ์•Œ๋ฆผ์„ ๋ฒ„ํŠผ ๊ณ **๋งํ’์„ **์œผ๋กœ โ€” ๋ˆ„๋ฅด๋ฉด ์ด๋™, ๋ฐ€๋ฉด ๋‹ซํž˜
103
105
  - ์ฝ์ง€ ์•Š์€ ์•Œ๋ฆผ ์ˆ˜๋ฅผ ๋ฒ„ํŠผ์— ํ‘œ์‹œ, ์ง€๋‚œ ์•Œ๋ฆผ๊ณผ ์ตœ๊ทผ ๊ฐค๋Ÿฌ๋ฆฌ๋Š” ํ•œ ํ™”๋ฉด์—
106
+ - PC๋Š” ๋‹ค๋ฅธ ํƒญ์„ ๋ณผ ๋•Œ **๊ธฐ๊ธฐ ์•Œ๋ฆผ**์œผ๋กœ๋„ (์„ ํƒ)
104
107
  - ๊ฐœ์ˆ˜ ์ œํ•œ ์—†๋Š” ์Šคํฌ๋žฉ, ํ”Œ๋กœํŒ… ๋ฉ”๋‰ด์—์„œ ๋ฐ”๋กœ ๋ณด๊ธฐ
105
108
  - ๊ธฐ๊ธฐ ๋‹คํฌ ๋ชจ๋“œ์— ๋งž์ถฐ ์‚ฌ์ดํŠธ ์•ผ๊ฐ„ ๋ชจ๋“œ ์ž๋™ ์ „ํ™˜
106
109
  - ๋ชจ๋“  ์„ค์ •์€ ์ผœ๊ณ  ๋Œ ์ˆ˜ ์žˆ์Œ
@@ -2,7 +2,7 @@
2
2
  // @name DCInside Enhancer
3
3
  // @name:ko ๋””์‹œ ์ธํ•ธ์„œ
4
4
  // @namespace https://github.com/kangsicj/DCInsideEnhancer
5
- // @version 1.1.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
@@ -2,7 +2,7 @@
2
2
  // @name DCInside Enhancer
3
3
  // @name:ko ๋””์‹œ ์ธํ•ธ์„œ
4
4
  // @namespace https://github.com/kangsicj/DCInsideEnhancer
5
- // @version 1.1.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.1.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
 
@@ -108,6 +108,7 @@
108
108
  recent: 'dc_recent_galls',
109
109
  recentNative: 'dc_recent_native',
110
110
  searchHistory: 'dc_search_history',
111
+ blockHits: 'dc_block_hits',
111
112
  alarmSeen: 'dc_alarm_marks',
112
113
  seenVersion: 'dc_seen_version'
113
114
  };
@@ -224,6 +225,8 @@
224
225
  alarmBubble: true,
225
226
  alarmKeepPolling: true,
226
227
  keyboardShortcuts: true,
228
+ hoverPreview: true,
229
+ alarmSystemNotify: false,
227
230
  pcFitWidth: true,
228
231
  autoHideScrollbar: true,
229
232
  scrapIntercept: true,
@@ -1851,6 +1854,159 @@
1851
1854
 
1852
1855
  #dc-custom-modal .dc-row.dc-row-new .dc-row-title { font-weight: 800; }
1853
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
+
1854
2010
  .dc-key-cursor { outline: 2px solid var(--dc-accent-ink) !important; outline-offset: -2px; background: var(--dc-accent-soft) !important; }
1855
2011
 
1856
2012
  #dc-custom-modal .dc-about {
@@ -6462,7 +6618,97 @@
6462
6618
 
6463
6619
  const currentPostKey = () => extractPostPath(location.href) || location.pathname;
6464
6620
 
6465
- const computeBlockReason = el => {
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 => {
6466
6712
  const hits = buildBlockKeys({
6467
6713
  nickname: el.dataset.dcNick || '',
6468
6714
  ip: el.dataset.dcIp || '',
@@ -9987,6 +10233,7 @@
9987
10233
  });
9988
10234
 
9989
10235
  area.appendChild(seg);
10236
+ renderCleanupHelper(area);
9990
10237
  (renderBlockSections[blockSection] || renderBlockSections.keywords)(area);
9991
10238
  };
9992
10239
 
@@ -10173,6 +10420,15 @@
10173
10420
  off: '์‚ฌ์ดํŠธ ๊ธฐ๋ณธ ํ•œ ์ค„ ์•Œ๋ฆผ์„ ๊ทธ๋Œ€๋กœ ์”๋‹ˆ๋‹ค.',
10174
10421
  refresh: 'root'
10175
10422
  },
10423
+ {
10424
+ group: 'alarm',
10425
+ key: 'alarmSystemNotify',
10426
+ parent: 'alarmBubble',
10427
+ name: '๋‹ค๋ฅธ ํƒญ์„ ๋ณผ ๋•Œ ๊ธฐ๊ธฐ ์•Œ๋ฆผ์œผ๋กœ๋„ (PC)',
10428
+ on: '์ด ํƒญ์„ ๋ณด๊ณ  ์žˆ์ง€ ์•Š์„ ๋•Œ ๋””์‹œ ์•Œ๋ฆผ์ด ์˜ค๋ฉด ์œˆ๋„์šฐยท๋งฅ ์•Œ๋ฆผ์œผ๋กœ ๋„์›๋‹ˆ๋‹ค. ๋ˆ„๋ฅด๋ฉด ์ด ํƒญ์œผ๋กœ ๋Œ์•„์™€ ํ•ด๋‹น ๊ธ€๋กœ ๊ฐ‘๋‹ˆ๋‹ค. ์ฒ˜์Œ ์ผค ๋•Œ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๊ถŒํ•œ์„ ๋ฌป์Šต๋‹ˆ๋‹ค.',
10429
+ off: '์ด ํƒญ์„ ๋ณผ ๋•Œ ๋งํ’์„ ์œผ๋กœ๋งŒ ์•Œ๋ฆฝ๋‹ˆ๋‹ค.',
10430
+ refresh: 'root'
10431
+ },
10176
10432
  {
10177
10433
  group: 'alarm',
10178
10434
  key: 'alarmKeepPolling',
@@ -10206,6 +10462,14 @@
10206
10462
  off: '๋‹จ์ถ•ํ‚ค๋ฅผ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.',
10207
10463
  refresh: 'none'
10208
10464
  },
10465
+ {
10466
+ group: 'list',
10467
+ key: 'hoverPreview',
10468
+ name: '์ œ๋ชฉ์— ๋งˆ์šฐ์Šค๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ๊ธ€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ (PC)',
10469
+ on: '๋ชฉ๋ก ์ œ๋ชฉ ์œ„์— ์ž ๊น ๋จธ๋ฌด๋ฅด๋ฉด ๊ธ€ ์ „์ฒด๋ฅผ ์˜† ์ฐฝ์— ๋ณด์—ฌ ์ค๋‹ˆ๋‹ค. ์ฐฝ ์•ˆ์—์„œ ์Šคํฌ๋กคํ•ด ์ฝ๊ณ , ์Šคํฌ๋žฉยท์ž‘์„ฑ์ž ์ฐจ๋‹จ๋„ ๋ฐ”๋กœ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฐจ๋‹จํ•œ ์ด๋ฏธ์ง€ยท๋‹จ์–ด๋Š” ๋ฏธ๋ฆฌ๋ณด๊ธฐ์—์„œ๋„ ๊ฐ€๋ฆฝ๋‹ˆ๋‹ค. ํ‚ค๋ณด๋“œ๋กœ ๊ณ ๋ฅธ ๊ธ€์€ P ๋กœ ์—ฝ๋‹ˆ๋‹ค.',
10470
+ off: '์ œ๋ชฉ์„ ๋ˆŒ๋Ÿฌ์•ผ ๊ธ€์„ ๋ด…๋‹ˆ๋‹ค.',
10471
+ refresh: 'root'
10472
+ },
10209
10473
  {
10210
10474
  group: 'list',
10211
10475
  key: 'autoFillList',
@@ -10403,6 +10667,20 @@
10403
10667
  paintAlarmBadge();
10404
10668
  }
10405
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
+
10406
10684
  break;
10407
10685
 
10408
10686
  case 'log':
@@ -11626,7 +11904,8 @@
11626
11904
  scraps: scraps.map(item => ({ ...item })),
11627
11905
  settings: syncedSettings(),
11628
11906
  recent: collectRecentForSync(),
11629
- search: collectSearchForSync()
11907
+ search: collectSearchForSync(),
11908
+ hits: collectHitsForSync()
11630
11909
  });
11631
11910
 
11632
11911
  const skippedFields = skip => SYNC_PARTS.filter(part => skip.includes(part.key)).flatMap(part => part.fields);
@@ -11734,7 +12013,8 @@
11734
12013
  scraps: mergeScraps(local.scraps, base && base.scraps, remote && remote.scraps),
11735
12014
  settings: mergeSettings(local.settings, base && base.settings, remote && remote.settings),
11736
12015
  recent: mergeRecent(local.recent, base && base.recent, remote && remote.recent),
11737
- search: mergeSearchHistory(local.search, base && base.search, remote && remote.search)
12016
+ search: mergeSearchHistory(local.search, base && base.search, remote && remote.search),
12017
+ hits: mergeBlockHits(local.hits, remote && remote.hits)
11738
12018
  };
11739
12019
 
11740
12020
  skippedFields(skip).forEach(field => {
@@ -11789,6 +12069,7 @@
11789
12069
 
11790
12070
  delete data.recent;
11791
12071
  delete data.search;
12072
+ delete data.hits;
11792
12073
 
11793
12074
  return JSON.stringify(data);
11794
12075
  };
@@ -11796,6 +12077,7 @@
11796
12077
  const applySyncData = data => {
11797
12078
  const recentChanged = data && 'recent' in data ? applyRecentFromSync(data.recent) : false;
11798
12079
  const searchChanged = data && 'search' in data ? applySearchFromSync(data.search) : false;
12080
+ const hitsChanged = data && 'hits' in data ? applyHitsFromSync(data.hits) : false;
11799
12081
  const clean = sanitizeBackup({ app: BACKUP_APP, ...data });
11800
12082
  const before = blockSyncData();
11801
12083
 
@@ -11850,7 +12132,7 @@
11850
12132
  refreshAfterBlockChange();
11851
12133
  }
11852
12134
 
11853
- return changed || recentChanged || searchChanged;
12135
+ return changed || recentChanged || searchChanged || hitsChanged;
11854
12136
  };
11855
12137
 
11856
12138
  const saveSyncUndo = (before, incoming, summary, from) => {
@@ -13751,7 +14033,9 @@
13751
14033
  let alarmWatchTimer = null;
13752
14034
 
13753
14035
  const checkSiteAlarms = () => {
13754
- if (settings.alarmBubble === false || document.visibilityState === 'hidden') {
14036
+ const away = document.visibilityState === 'hidden' || (typeof document.hasFocus === 'function' && !document.hasFocus());
14037
+
14038
+ if (settings.alarmBubble === false || (document.visibilityState === 'hidden' && !canSystemNotify())) {
13755
14039
  return;
13756
14040
  }
13757
14041
 
@@ -13779,7 +14063,11 @@
13779
14063
  writeAlarmMarks({ shown: latest });
13780
14064
  }
13781
14065
 
13782
- 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') {
13783
14071
  showAlarmBubble(fresh[0], fresh.length > ALARM_BUBBLE_MAX ? fresh.length - 1 : 0);
13784
14072
 
13785
14073
  if (fresh.length > 1 && fresh.length <= ALARM_BUBBLE_MAX) {
@@ -13990,7 +14278,7 @@
13990
14278
 
13991
14279
  // =========================================================
13992
14280
 
13993
- const KEY_HELP = 'J/K ๋‹ค์Œยท์ด์ „ ๊ธ€ ยท Enter ์—ด๊ธฐ ยท S ์Šคํฌ๋žฉ ยท / ๊ฒ€์ƒ‰ ยท B ์ฐจ๋‹จ ๊ด€๋ฆฌ';
14281
+ const KEY_HELP = 'J/K ๋‹ค์Œยท์ด์ „ ๊ธ€ ยท Enter ์—ด๊ธฐ ยท P ๋ฏธ๋ฆฌ๋ณด๊ธฐ ยท S ์Šคํฌ๋žฉ ยท / ๊ฒ€์ƒ‰ ยท B ์ฐจ๋‹จ ๊ด€๋ฆฌ';
13994
14282
 
13995
14283
  const typingTarget = el =>
13996
14284
  !!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) || !!el.closest?.('[contenteditable="true"]'));
@@ -14091,6 +14379,8 @@
14091
14379
  } else if (key === 'b' || key === 'B') {
14092
14380
  openBlockModal(document.getElementById('dc-custom-fab')?.dataset.dcAlarm ? 'recent' : 'overview');
14093
14381
  handled = true;
14382
+ } else if ((key === 'p' || key === 'P') && togglePreviewForCursor()) {
14383
+ handled = true;
14094
14384
  } else if (key === '?') {
14095
14385
  showToast(`โŒจ๏ธ ${KEY_HELP}`, { duration: 6000 });
14096
14386
  handled = true;
@@ -14159,6 +14449,713 @@
14159
14449
 
14160
14450
  return true;
14161
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
+ };
14162
15159
  // =========================================================
14163
15160
  // 14. ํ”Œ๋กœํŒ… ๋ฒ„ํŠผ
14164
15161
  // =========================================================
@@ -15147,6 +16144,7 @@
15147
16144
  initRecent();
15148
16145
  initSiteAlarms();
15149
16146
  initKeyboardShortcuts();
16147
+ initHoverPreview();
15150
16148
  restoreListSnapshot();
15151
16149
  processElements();
15152
16150
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangsicj/dcinsideenhancer",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "DCInside filter & enhancer userscript for mobile and PC (๋””์‹œ์ธ์‚ฌ์ด๋“œ ์ฐจ๋‹จยทํŽธ์˜ ์œ ์ €์Šคํฌ๋ฆฝํŠธ)",
5
5
  "license": "MIT",
6
6
  "author": "kangsicj",